Enforce semicolons in eslint

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
Panu Horsmalahti committed 2020-11-19 16:35:04 +02:00
1 parent 0f3f5611ba
commit 1477bb8274
531 files changed
+3979 -3975

No files matched your search

+60 -60
View File
@@ -21,35 +21,35 @@ jest.mock("winston", () => ({
Console: jest.fn(),
File: jest.fn(),
}
}))
}));
jest.mock("../../common/ipc")
jest.mock("../context-handler")
jest.mock("request")
jest.mock("request-promise-native")
jest.mock("../../common/ipc");
jest.mock("../context-handler");
jest.mock("request");
jest.mock("request-promise-native");
import { Console } from "console";
import mockFs from "mock-fs";
import { workspaceStore } from "../../common/workspace-store";
import { Cluster } from "../cluster"
import { Cluster } from "../cluster";
import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port";
import { V1ResourceAttributes } from "@kubernetes/client-node";
import { apiResources } from "../../common/rbac";
import request from "request-promise-native"
import request from "request-promise-native";
import { Kubectl } from "../kubectl";
const mockedRequest = request as jest.MockedFunction<typeof request>
const mockedRequest = request as jest.MockedFunction<typeof request>;
console = new Console(process.stdout, process.stderr) // fix mockFS
console = new Console(process.stdout, process.stderr); // fix mockFS
describe("create clusters", () => {
beforeEach(() => {
jest.clearAllMocks()
})
jest.clearAllMocks();
});
let c: Cluster
let c: Cluster;
beforeEach(() => {
const mockOpts = {
@@ -74,68 +74,68 @@ describe("create clusters", () => {
kind: "Config",
preferences: {},
})
}
mockFs(mockOpts)
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true))
};
mockFs(mockOpts);
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
c = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
})
});
});
afterEach(() => {
mockFs.restore()
})
mockFs.restore();
});
it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => {
expect(c.apiUrl).toBe("https://192.168.64.3:8443")
})
expect(c.apiUrl).toBe("https://192.168.64.3:8443");
});
it("reconnect should not throw if contextHandler is missing", () => {
expect(() => c.reconnect()).not.toThrowError()
})
expect(() => c.reconnect()).not.toThrowError();
});
it("disconnect should not throw if contextHandler is missing", () => {
expect(() => c.disconnect()).not.toThrowError()
})
expect(() => c.disconnect()).not.toThrowError();
});
it("init should not throw if everything is in order", async () => {
await c.init(await getFreePort())
await c.init(await getFreePort());
expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), {
id: "foo",
apiUrl: "https://192.168.64.3:8443",
context: "minikube",
})
})
});
});
it("activating cluster should try to connect to cluster and do a refresh", async () => {
const port = await getFreePort()
const port = await getFreePort();
jest.spyOn(ContextHandler.prototype, "ensureServer");
const mockListNSs = jest.fn()
const mockListNSs = jest.fn();
const mockKC = {
makeApiClient() {
return {
listNamespace: mockListNSs,
}
};
}
}
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true))
};
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
jest.spyOn(Cluster.prototype, "canI")
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
expect(attr.namespace).toBe("default")
expect(attr.resource).toBe("pods")
expect(attr.verb).toBe("list")
return Promise.resolve(true)
expect(attr.namespace).toBe("default");
expect(attr.resource).toBe("pods");
expect(attr.verb).toBe("list");
return Promise.resolve(true);
})
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
expect(attr.namespace).toBe("default")
expect(attr.verb).toBe("list")
return Promise.resolve(true)
})
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any)
expect(attr.namespace).toBe("default");
expect(attr.verb).toBe("list");
return Promise.resolve(true);
});
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any);
mockListNSs.mockImplementationOnce(() => ({
body: {
items: [{
@@ -144,36 +144,36 @@ describe("create clusters", () => {
}
}]
}
}))
}));
mockedRequest.mockImplementationOnce(((uri: any, _options: any) => {
expect(uri).toBe(`http://localhost:${port}/api-kube/version`)
return Promise.resolve({ gitVersion: "1.2.3" })
}) as any)
expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
return Promise.resolve({ gitVersion: "1.2.3" });
}) as any);
const c = new class extends Cluster {
// only way to mock protected methods, without these we leak promises
protected bindEvents() {
return
return;
}
protected async ensureKubectl() {
return Promise.resolve(true)
return Promise.resolve(true);
}
}({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
await c.init(port)
await c.activate()
});
await c.init(port);
await c.activate();
expect(ContextHandler.prototype.ensureServer).toBeCalled()
expect(mockedRequest).toBeCalled()
expect(c.accessible).toBe(true)
expect(c.allowedNamespaces.length).toBe(1)
expect(c.allowedResources.length).toBe(apiResources.length)
c.disconnect()
jest.resetAllMocks()
})
})
expect(ContextHandler.prototype.ensureServer).toBeCalled();
expect(mockedRequest).toBeCalled();
expect(c.accessible).toBe(true);
expect(c.allowedNamespaces.length).toBe(1);
expect(c.allowedResources.length).toBe(apiResources.length);
c.disconnect();
jest.resetAllMocks();
});
});
+72 -72
View File
@@ -21,109 +21,109 @@ jest.mock("winston", () => ({
Console: jest.fn(),
File: jest.fn(),
}
}))
}));
jest.mock("../../common/ipc")
jest.mock("child_process")
jest.mock("tcp-port-used")
jest.mock("../../common/ipc");
jest.mock("child_process");
jest.mock("tcp-port-used");
import { Cluster } from "../cluster"
import { KubeAuthProxy } from "../kube-auth-proxy"
import { getFreePort } from "../port"
import { broadcastMessage } from "../../common/ipc"
import { ChildProcess, spawn, SpawnOptions } from "child_process"
import { bundledKubectlPath, Kubectl } from "../kubectl"
import { Cluster } from "../cluster";
import { KubeAuthProxy } from "../kube-auth-proxy";
import { getFreePort } from "../port";
import { broadcastMessage } from "../../common/ipc";
import { ChildProcess, spawn, SpawnOptions } from "child_process";
import { bundledKubectlPath, Kubectl } from "../kubectl";
import { mock, MockProxy } from 'jest-mock-extended';
import { waitUntilUsed } from 'tcp-port-used';
import { Readable } from "stream"
import { Readable } from "stream";
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>
const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>;
describe("kube auth proxy tests", () => {
beforeEach(() => {
jest.clearAllMocks()
})
jest.clearAllMocks();
});
it("calling exit multiple times shouldn't throw", async () => {
const port = await getFreePort()
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
kap.exit()
kap.exit()
kap.exit()
})
const port = await getFreePort();
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {});
kap.exit();
kap.exit();
kap.exit();
});
describe("spawn tests", () => {
let port: number
let mockedCP: MockProxy<ChildProcess>
let listeners: Record<string, (...args: any[]) => void>
let proxy: KubeAuthProxy
let port: number;
let mockedCP: MockProxy<ChildProcess>;
let listeners: Record<string, (...args: any[]) => void>;
let proxy: KubeAuthProxy;
beforeEach(async () => {
port = await getFreePort()
mockedCP = mock<ChildProcess>()
listeners = {}
port = await getFreePort();
mockedCP = mock<ChildProcess>();
listeners = {};
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true))
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false))
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
listeners[event] = listener
return mockedCP
})
mockedCP.stderr = mock<Readable>()
listeners[event] = listener;
return mockedCP;
});
mockedCP.stderr = mock<Readable>();
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stderr/${event}`] = listener
return mockedCP.stderr
})
mockedCP.stdout = mock<Readable>()
listeners[`stderr/${event}`] = listener;
return mockedCP.stderr;
});
mockedCP.stdout = mock<Readable>();
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stdout/${event}`] = listener
return mockedCP.stdout
})
listeners[`stdout/${event}`] = listener;
return mockedCP.stdout;
});
mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => {
expect(command).toBe(bundledKubectlPath())
return mockedCP
})
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve())
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" })
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal")
proxy = new KubeAuthProxy(cluster, port, {})
})
expect(command).toBe(bundledKubectlPath());
return mockedCP;
});
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" });
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal");
proxy = new KubeAuthProxy(cluster, port, {});
});
it("should call spawn and broadcast errors", async () => {
await proxy.run()
listeners["error"]({ message: "foobarbat" })
await proxy.run();
listeners["error"]({ message: "foobarbat" });
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "foobarbat", error: true })
})
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "foobarbat", error: true });
});
it("should call spawn and broadcast exit", async () => {
await proxy.run()
listeners["exit"](0)
await proxy.run();
listeners["exit"](0);
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "proxy exited with code: 0", error: false })
})
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "proxy exited with code: 0", error: false });
});
it("should call spawn and broadcast errors from stderr", async () => {
await proxy.run()
listeners["stderr/data"]("an error")
await proxy.run();
listeners["stderr/data"]("an error");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "an error", error: true })
})
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "an error", error: true });
});
it("should call spawn and broadcast stdout serving info", async () => {
await proxy.run()
listeners["stdout/data"]("Starting to serve on")
await proxy.run();
listeners["stdout/data"]("Starting to serve on");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "Authentication proxy started\n" })
})
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "Authentication proxy started\n" });
});
it("should call spawn and broadcast stdout other info", async () => {
await proxy.run()
listeners["stdout/data"]("some info")
await proxy.run();
listeners["stdout/data"]("some info");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "some info" })
})
})
})
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "some info" });
});
});
});
+36 -36
View File
@@ -21,24 +21,24 @@ jest.mock("winston", () => ({
Console: jest.fn(),
File: jest.fn(),
}
}))
}));
import { KubeconfigManager } from "../kubeconfig-manager"
import mockFs from "mock-fs"
import { KubeconfigManager } from "../kubeconfig-manager";
import mockFs from "mock-fs";
import { Cluster } from "../cluster";
import { workspaceStore } from "../../common/workspace-store";
import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port";
import fse from "fs-extra"
import fse from "fs-extra";
import { loadYaml } from "@kubernetes/client-node";
import { Console } from "console";
console = new Console(process.stdout, process.stderr) // fix mockFS
console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => {
beforeEach(() => {
jest.clearAllMocks()
})
jest.clearAllMocks();
});
beforeEach(() => {
const mockOpts = {
@@ -63,13 +63,13 @@ describe("kubeconfig manager tests", () => {
kind: "Config",
preferences: {},
})
}
mockFs(mockOpts)
})
};
mockFs(mockOpts);
});
afterEach(() => {
mockFs.restore()
})
mockFs.restore();
});
it("should create 'temp' kube config with proxy", async () => {
const cluster = new Cluster({
@@ -77,19 +77,19 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
const contextHandler = new ContextHandler(cluster)
const port = await getFreePort()
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
});
const contextHandler = new ContextHandler(cluster);
const port = await getFreePort();
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
expect(logger.error).not.toBeCalled()
expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo")
const file = await fse.readFile(kubeConfManager.getPath())
const yml = loadYaml<any>(file.toString())
expect(yml["current-context"]).toBe("minikube")
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`)
expect(yml["users"][0]["name"]).toBe("proxy")
})
expect(logger.error).not.toBeCalled();
expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo");
const file = await fse.readFile(kubeConfManager.getPath());
const yml = loadYaml<any>(file.toString());
expect(yml["current-context"]).toBe("minikube");
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`);
expect(yml["users"][0]["name"]).toBe("proxy");
});
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const cluster = new Cluster({
@@ -97,16 +97,16 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
const contextHandler = new ContextHandler(cluster)
const port = await getFreePort()
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
});
const contextHandler = new ContextHandler(cluster);
const port = await getFreePort();
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
const configPath = kubeConfManager.getPath()
expect(await fse.pathExists(configPath)).toBe(true)
await kubeConfManager.unlink()
expect(await fse.pathExists(configPath)).toBe(false)
await kubeConfManager.unlink() // doesn't throw
expect(kubeConfManager.getPath()).toBeUndefined()
})
})
const configPath = kubeConfManager.getPath();
expect(await fse.pathExists(configPath)).toBe(true);
await kubeConfManager.unlink();
expect(await fse.pathExists(configPath)).toBe(false);
await kubeConfManager.unlink(); // doesn't throw
expect(kubeConfManager.getPath()).toBeUndefined();
});
});
+5 -5
View File
@@ -1,19 +1,19 @@
import { autoUpdater } from "electron-updater"
import logger from "./logger"
import { autoUpdater } from "electron-updater";
import logger from "./logger";
export class AppUpdater {
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day
static checkForUpdates() {
return autoUpdater.checkForUpdatesAndNotify()
return autoUpdater.checkForUpdatesAndNotify();
}
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
autoUpdater.logger = logger
autoUpdater.logger = logger;
}
public start() {
setInterval(AppUpdater.checkForUpdates, this.updateInterval)
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
return AppUpdater.checkForUpdates();
}
}
@@ -1,4 +1,4 @@
import request, { RequestPromiseOptions } from "request-promise-native"
import request, { RequestPromiseOptions } from "request-promise-native";
import { Cluster } from "../cluster";
export type ClusterDetectionResult = {
@@ -11,11 +11,11 @@ export class BaseClusterDetector {
key: string
constructor(cluster: Cluster) {
this.cluster = cluster
this.cluster = cluster;
}
detect(): Promise<ClusterDetectionResult> {
return null
return null;
}
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
@@ -28,6 +28,6 @@ export class BaseClusterDetector {
Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
...(options.headers || {}),
},
})
});
}
}
@@ -1,23 +1,23 @@
import { BaseClusterDetector } from "./base-cluster-detector";
import { createHash } from "crypto"
import { createHash } from "crypto";
import { ClusterMetadataKey } from "../cluster";
export class ClusterIdDetector extends BaseClusterDetector {
key = ClusterMetadataKey.CLUSTER_ID
public async detect() {
let id: string
let id: string;
try {
id = await this.getDefaultNamespaceId()
id = await this.getDefaultNamespaceId();
} catch(_) {
id = this.cluster.apiUrl
id = this.cluster.apiUrl;
}
const value = createHash("sha256").update(id).digest("hex")
return { value: value, accuracy: 100 }
const value = createHash("sha256").update(id).digest("hex");
return { value: value, accuracy: 100 };
}
protected async getDefaultNamespaceId() {
const response = await this.k8sRequest("/api/v1/namespaces/default")
return response.metadata.uid
const response = await this.k8sRequest("/api/v1/namespaces/default");
return response.metadata.uid;
}
}
+15 -15
View File
@@ -12,34 +12,34 @@ export class DetectorRegistry {
registry = observable.array<typeof BaseClusterDetector>([], { deep: false });
add(detectorClass: typeof BaseClusterDetector) {
this.registry.push(detectorClass)
this.registry.push(detectorClass);
}
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
const results: {[key: string]: ClusterDetectionResult } = {}
const results: {[key: string]: ClusterDetectionResult } = {};
for (const detectorClass of this.registry) {
const detector = new detectorClass(cluster)
const detector = new detectorClass(cluster);
try {
const data = await detector.detect()
const data = await detector.detect();
if (!data) continue;
const existingValue = results[detector.key]
const existingValue = results[detector.key];
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
results[detector.key] = data
results[detector.key] = data;
} catch (e) {
// detector raised error, do nothing
}
}
const metadata: ClusterMetadata = {}
const metadata: ClusterMetadata = {};
for (const [key, result] of Object.entries(results)) {
metadata[key] = result.value
metadata[key] = result.value;
}
return metadata
return metadata;
}
}
export const detectorRegistry = new DetectorRegistry()
detectorRegistry.add(ClusterIdDetector)
detectorRegistry.add(LastSeenDetector)
detectorRegistry.add(VersionDetector)
detectorRegistry.add(DistributionDetector)
detectorRegistry.add(NodesCountDetector)
export const detectorRegistry = new DetectorRegistry();
detectorRegistry.add(ClusterIdDetector);
detectorRegistry.add(LastSeenDetector);
detectorRegistry.add(VersionDetector);
detectorRegistry.add(DistributionDetector);
detectorRegistry.add(NodesCountDetector);
@@ -6,75 +6,75 @@ export class DistributionDetector extends BaseClusterDetector {
version: string
public async detect() {
this.version = await this.getKubernetesVersion()
this.version = await this.getKubernetesVersion();
if (await this.isRancher()) {
return { value: "rancher", accuracy: 80}
return { value: "rancher", accuracy: 80};
}
if (this.isGKE()) {
return { value: "gke", accuracy: 80}
return { value: "gke", accuracy: 80};
}
if (this.isEKS()) {
return { value: "eks", accuracy: 80}
return { value: "eks", accuracy: 80};
}
if (this.isIKS()) {
return { value: "iks", accuracy: 80}
return { value: "iks", accuracy: 80};
}
if (this.isAKS()) {
return { value: "aks", accuracy: 80}
return { value: "aks", accuracy: 80};
}
if (this.isDigitalOcean()) {
return { value: "digitalocean", accuracy: 90}
return { value: "digitalocean", accuracy: 90};
}
if (this.isMinikube()) {
return { value: "minikube", accuracy: 80}
return { value: "minikube", accuracy: 80};
}
if (this.isCustom()) {
return { value: "custom", accuracy: 10}
return { value: "custom", accuracy: 10};
}
return { value: "unknown", accuracy: 10}
return { value: "unknown", accuracy: 10};
}
public async getKubernetesVersion() {
if (this.cluster.version) return this.cluster.version
if (this.cluster.version) return this.cluster.version;
const response = await this.k8sRequest("/version")
return response.gitVersion
const response = await this.k8sRequest("/version");
return response.gitVersion;
}
protected isGKE() {
return this.version.includes("gke")
return this.version.includes("gke");
}
protected isEKS() {
return this.version.includes("eks")
return this.version.includes("eks");
}
protected isIKS() {
return this.version.includes("IKS")
return this.version.includes("IKS");
}
protected isAKS() {
return this.cluster.apiUrl.endsWith("azmk8s.io")
return this.cluster.apiUrl.endsWith("azmk8s.io");
}
protected isDigitalOcean() {
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com")
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
}
protected isMinikube() {
return this.cluster.contextName.startsWith("minikube")
return this.cluster.contextName.startsWith("minikube");
}
protected isCustom() {
return this.version.includes("+")
return this.version.includes("+");
}
protected async isRancher() {
try {
const response = await this.k8sRequest("")
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined
const response = await this.k8sRequest("");
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined;
} catch (e) {
return false
return false;
}
}
}
@@ -7,7 +7,7 @@ export class LastSeenDetector extends BaseClusterDetector {
public async detect() {
if (!this.cluster.accessible) return null;
await this.k8sRequest("/version")
return { value: new Date().toJSON(), accuracy: 100 }
await this.k8sRequest("/version");
return { value: new Date().toJSON(), accuracy: 100 };
}
}
@@ -6,12 +6,12 @@ export class NodesCountDetector extends BaseClusterDetector {
public async detect() {
if (!this.cluster.accessible) return null;
const nodeCount = await this.getNodeCount()
return { value: nodeCount, accuracy: 100}
const nodeCount = await this.getNodeCount();
return { value: nodeCount, accuracy: 100};
}
protected async getNodeCount(): Promise<number> {
const response = await this.k8sRequest("/api/v1/nodes")
return response.items.length
const response = await this.k8sRequest("/api/v1/nodes");
return response.items.length;
}
}
@@ -6,12 +6,12 @@ export class VersionDetector extends BaseClusterDetector {
value: string
public async detect() {
const version = await this.getKubernetesVersion()
return { value: version, accuracy: 100}
const version = await this.getKubernetesVersion();
return { value: version, accuracy: 100};
}
public async getKubernetesVersion() {
const response = await this.k8sRequest("/version")
return response.gitVersion
const response = await this.k8sRequest("/version");
return response.gitVersion;
}
}
+22 -22
View File
@@ -1,16 +1,16 @@
import "../common/cluster-ipc";
import type http from "http"
import { ipcMain } from "electron"
import type http from "http";
import { ipcMain } from "electron";
import { autorun } from "mobx";
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"
import { Cluster } from "./cluster"
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
import { Cluster } from "./cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
import { Singleton } from "../common/utils";
export class ClusterManager extends Singleton {
constructor(public readonly port: number) {
super()
super();
// auto-init clusters
autorun(() => {
clusterStore.enabledClustersList.forEach(cluster => {
@@ -34,52 +34,52 @@ export class ClusterManager extends Singleton {
delay: 250
});
ipcMain.on("network:offline", () => { this.onNetworkOffline() })
ipcMain.on("network:online", () => { this.onNetworkOnline() })
ipcMain.on("network:offline", () => { this.onNetworkOffline(); });
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
}
protected onNetworkOffline() {
logger.info("[CLUSTER-MANAGER]: network is offline")
logger.info("[CLUSTER-MANAGER]: network is offline");
clusterStore.enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.online = false
cluster.accessible = false
cluster.refreshConnectionStatus().catch((e) => e)
cluster.online = false;
cluster.accessible = false;
cluster.refreshConnectionStatus().catch((e) => e);
}
})
});
}
protected onNetworkOnline() {
logger.info("[CLUSTER-MANAGER]: network is online")
logger.info("[CLUSTER-MANAGER]: network is online");
clusterStore.enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e)
cluster.refreshConnectionStatus().catch((e) => e);
}
})
});
}
stop() {
clusterStore.clusters.forEach((cluster: Cluster) => {
cluster.disconnect();
})
});
}
getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null
let cluster: Cluster = null;
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1]
cluster = clusterStore.getById(clusterId)
const clusterId = req.url.split("/")[1];
cluster = clusterStore.getById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
} else if (req.headers["x-cluster-id"]) {
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString())
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
} else {
const clusterId = getClusterIdFromHost(req.headers.host);
cluster = clusterStore.getById(clusterId)
cluster = clusterStore.getById(clusterId);
}
return cluster;
+65 -65
View File
@@ -1,18 +1,18 @@
import { ipcMain } from "electron"
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import { ipcMain } from "electron";
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store";
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { broadcastMessage } from "../common/ipc";
import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import { ContextHandler } from "./context-handler";
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager"
import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers"
import request, { RequestPromiseOptions } from "request-promise-native"
import { KubeconfigManager } from "./kubeconfig-manager";
import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers";
import request, { RequestPromiseOptions } from "request-promise-native";
import { apiResources } from "../common/rbac";
import logger from "./logger"
import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry";
@@ -86,23 +86,23 @@ export class Cluster implements ClusterModel, ClusterState {
}
@computed get name() {
return this.preferences.clusterName || this.contextName
return this.preferences.clusterName || this.contextName;
}
get version(): string {
return String(this.metadata?.version) || ""
return String(this.metadata?.version) || "";
}
constructor(model: ClusterModel) {
this.updateModel(model);
const kubeconfig = this.getKubeconfig()
const kubeconfig = this.getKubeconfig();
if (kubeconfig.getContextObject(this.contextName)) {
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
}
}
get isManaged(): boolean {
return !!this.ownerRef
return !!this.ownerRef;
}
@action
@@ -131,16 +131,16 @@ export class Cluster implements ClusterModel, ClusterState {
}
protected bindEvents() {
logger.info(`[CLUSTER]: bind events`, this.getMeta())
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000) // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000) // every 15 minutes
logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
if (ipcMain) {
this.eventDisposers.push(
reaction(() => this.getState(), () => this.pushState()),
() => {
clearInterval(refreshTimer)
clearInterval(refreshMetadataTimer)
clearInterval(refreshTimer);
clearInterval(refreshMetadataTimer);
},
);
}
@@ -165,20 +165,20 @@ export class Cluster implements ClusterModel, ClusterState {
if (this.disconnected || !this.accessible) {
await this.reconnect();
}
await this.refreshConnectionStatus()
await this.refreshConnectionStatus();
if (this.accessible) {
await this.refreshAllowedResources()
this.isAdmin = await this.isClusterAdmin()
this.ready = true
this.ensureKubectl()
await this.refreshAllowedResources();
this.isAdmin = await this.isClusterAdmin();
this.ready = true;
this.ensureKubectl();
}
this.activated = true
this.activated = true;
return this.pushState();
}
protected async ensureKubectl() {
this.kubeCtl = new Kubectl(this.version)
return this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
this.kubeCtl = new Kubectl(this.version);
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
}
@action
@@ -214,9 +214,9 @@ export class Cluster implements ClusterModel, ClusterState {
this.refreshAllowedResources(),
]);
if (opts.refreshMetadata) {
this.refreshMetadata()
this.refreshMetadata();
}
this.ready = true
this.ready = true;
}
this.pushState();
}
@@ -224,9 +224,9 @@ export class Cluster implements ClusterModel, ClusterState {
@action
async refreshMetadata() {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await detectorRegistry.detectForCluster(this)
const existingMetadata = this.metadata
this.metadata = Object.assign(existingMetadata, metadata)
const metadata = await detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata;
this.metadata = Object.assign(existingMetadata, metadata);
}
@action
@@ -256,16 +256,16 @@ export class Cluster implements ClusterModel, ClusterState {
}
getProxyKubeconfigPath(): string {
return this.kubeconfigManager.getPath()
return this.kubeconfigManager.getPath();
}
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
options.headers ??= {}
options.json ??= true
options.timeout ??= 30000
options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}` // required in ClusterManager.getClusterForRequest()
options.headers ??= {};
options.json ??= true;
options.timeout ??= 30000;
options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
return request(this.kubeProxyUrl + path, options)
return request(this.kubeProxyUrl + path, options);
}
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
@@ -276,17 +276,17 @@ export class Cluster implements ClusterModel, ClusterState {
resolveWithFullResponse: false,
json: true,
qs: queryParams,
})
});
}
protected async getConnectionStatus(): Promise<ClusterStatus> {
try {
const versionDetector = new VersionDetector(this)
const versionData = await versionDetector.detect()
this.metadata.version = versionData.value
const versionDetector = new VersionDetector(this);
const versionData = await versionDetector.detect();
this.metadata.version = versionData.value;
return ClusterStatus.AccessGranted;
} catch (error) {
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`)
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials";
@@ -310,17 +310,17 @@ export class Cluster implements ClusterModel, ClusterState {
}
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api)
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
try {
const accessReview = await authApi.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes }
})
return accessReview.body.status.allowed
});
return accessReview.body.status.allowed;
} catch (error) {
logger.error(`failed to request selfSubjectAccessReview: ${error}`)
return false
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
return false;
}
}
@@ -329,7 +329,7 @@ export class Cluster implements ClusterModel, ClusterState {
namespace: "kube-system",
resource: "*",
verb: "create",
})
});
}
protected async getEventCount(): Promise<number> {
@@ -345,7 +345,7 @@ export class Cluster implements ClusterModel, ClusterState {
if (w.involvedObject.kind === 'Pod') {
try {
const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace);
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`);
if (podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid);
}
@@ -361,7 +361,7 @@ export class Cluster implements ClusterModel, ClusterState {
.reduce((sum, conditions) => sum + conditions.length, 0);
return uniqEventSources.size + nodeNotificationCount;
} catch (error) {
logger.error("Failed to fetch event count: " + JSON.stringify(error))
logger.error("Failed to fetch event count: " + JSON.stringify(error));
return 0;
}
}
@@ -379,7 +379,7 @@ export class Cluster implements ClusterModel, ClusterState {
};
return toJS(model, {
recurseEverything: true
})
});
}
// serializable cluster-state used for sync btw main <-> renderer
@@ -399,17 +399,17 @@ export class Cluster implements ClusterModel, ClusterState {
};
return toJS(state, {
recurseEverything: true
})
});
}
@action
setState(state: ClusterState) {
Object.assign(this, state)
Object.assign(this, state);
}
pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state);
broadcastMessage("cluster:state", this.id, state)
broadcastMessage("cluster:state", this.id, state);
}
// get cluster system meta, e.g. use in "logger"
@@ -422,30 +422,30 @@ export class Cluster implements ClusterModel, ClusterState {
online: this.online,
accessible: this.accessible,
disconnected: this.disconnected,
}
};
}
protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) {
return this.accessibleNamespaces
return this.accessibleNamespaces;
}
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
try {
const namespaceList = await api.listNamespace()
const namespaceList = await api.listNamespace();
const nsAccessStatuses = await Promise.all(
namespaceList.body.items.map(ns => this.canI({
namespace: ns.metadata.name,
resource: "pods",
verb: "list",
}))
)
);
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name)
.map(ns => ns.metadata.name);
} catch (error) {
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
if (ctx.namespace) return [ctx.namespace]
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
if (ctx.namespace) return [ctx.namespace];
return [];
}
}
@@ -462,12 +462,12 @@ export class Cluster implements ClusterModel, ClusterState {
verb: "list",
namespace: this.allowedNamespaces[0]
}))
)
);
return apiResources
.filter((resource, i) => resourceAccessStatuses[i])
.map(apiResource => apiResource.resource)
.map(apiResource => apiResource.resource);
} catch (error) {
return []
return [];
}
}
}
+37 -37
View File
@@ -1,13 +1,13 @@
import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry";
import type { ClusterPreferences } from "../common/cluster-store";
import type { Cluster } from "./cluster"
import type httpProxy from "http-proxy"
import type { Cluster } from "./cluster";
import type httpProxy from "http-proxy";
import url, { UrlWithStringQuery } from "url";
import { CoreV1Api } from "@kubernetes/client-node"
import { prometheusProviders } from "../common/prometheus-providers"
import logger from "./logger"
import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy"
import { CoreV1Api } from "@kubernetes/client-node";
import { prometheusProviders } from "../common/prometheus-providers";
import logger from "./logger";
import { getFreePort } from "./port";
import { KubeAuthProxy } from "./kube-auth-proxy";
export class ContextHandler {
public proxyPort: number;
@@ -26,64 +26,64 @@ export class ContextHandler {
this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheusPath = null;
if (preferences.prometheus) {
const { namespace, service, port } = preferences.prometheus
this.prometheusPath = `${namespace}/services/${service}:${port}`
const { namespace, service, port } = preferences.prometheus;
this.prometheusPath = `${namespace}/services/${service}:${port}`;
}
}
protected async resolvePrometheusPath(): Promise<string> {
const { service, namespace, port } = await this.getPrometheusService()
return `${namespace}/services/${service}:${port}`
const { service, namespace, port } = await this.getPrometheusService();
return `${namespace}/services/${service}:${port}`;
}
async getPrometheusProvider() {
if (!this.prometheusProvider) {
const service = await this.getPrometheusService()
logger.info(`using ${service.id} as prometheus provider`)
this.prometheusProvider = service.id
const service = await this.getPrometheusService();
logger.info(`using ${service.id} as prometheus provider`);
this.prometheusProvider = service.id;
}
return prometheusProviders.find(p => p.id === this.prometheusProvider)
return prometheusProviders.find(p => p.id === this.prometheusProvider);
}
async getPrometheusService(): Promise<PrometheusService> {
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api)
return await provider.getPrometheusService(apiClient)
})
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
return await provider.getPrometheusService(apiClient);
});
const resolvedPrometheusServices = await Promise.all(prometheusPromises);
const service = resolvedPrometheusServices.filter(n => n)[0];
return service || {
id: "lens",
namespace: "lens-metrics",
service: "prometheus",
port: 80
}
};
}
async getPrometheusPath(): Promise<string> {
if (!this.prometheusPath) {
this.prometheusPath = await this.resolvePrometheusPath()
this.prometheusPath = await this.resolvePrometheusPath();
}
return this.prometheusPath;
}
async resolveAuthProxyUrl() {
const proxyPort = await this.ensurePort();
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : ""
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
return `http://127.0.0.1:${proxyPort}${path}`;
}
async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
if (this.apiTarget && !isWatchRequest) {
return this.apiTarget
return this.apiTarget;
}
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000 // 4 hours for watch request, 30 seconds for the rest
const apiTarget = await this.newApiTarget(timeout)
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
const apiTarget = await this.newApiTarget(timeout);
if (!isWatchRequest) {
this.apiTarget = apiTarget
this.apiTarget = apiTarget;
}
return apiTarget
return apiTarget;
}
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
@@ -95,36 +95,36 @@ export class ContextHandler {
headers: {
"Host": this.clusterUrl.hostname,
},
}
};
}
async ensurePort(): Promise<number> {
if (!this.proxyPort) {
this.proxyPort = await getFreePort();
}
return this.proxyPort
return this.proxyPort;
}
async ensureServer() {
if (!this.kubeAuthProxy) {
await this.ensurePort();
const proxyEnv = Object.assign({}, process.env)
const proxyEnv = Object.assign({}, process.env);
if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
}
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv)
await this.kubeAuthProxy.run()
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv);
await this.kubeAuthProxy.run();
}
}
stopServer() {
if (this.kubeAuthProxy) {
this.kubeAuthProxy.exit()
this.kubeAuthProxy = null
this.kubeAuthProxy.exit();
this.kubeAuthProxy = null;
}
}
get proxyLastError(): string {
return this.kubeAuthProxy?.lastError || ""
return this.kubeAuthProxy?.lastError || "";
}
}
+5 -5
View File
@@ -6,13 +6,13 @@ import logger from "./logger";
export function exitApp() {
const windowManager = WindowManager.getInstance<WindowManager>()
const clusterManager = ClusterManager.getInstance<ClusterManager>()
appEventBus.emit({ name: "service", action: "close" })
const windowManager = WindowManager.getInstance<WindowManager>();
const clusterManager = ClusterManager.getInstance<ClusterManager>();
appEventBus.emit({ name: "service", action: "close" });
windowManager.hide();
clusterManager.stop();
logger.info('SERVICE:QUIT');
setTimeout(() => {
app.exit()
}, 1000)
app.exit();
}, 1000);
}
+28 -28
View File
@@ -1,9 +1,9 @@
import fs from "fs";
import * as yaml from "js-yaml";
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager";
import logger from "../logger";
import { promiseExec } from "../promise-exec"
import { helmCli } from "./helm-cli"
import { promiseExec } from "../promise-exec";
import { helmCli } from "./helm-cli";
type CachedYaml = {
entries: any; // todo: types
@@ -14,61 +14,61 @@ export class HelmChartManager {
protected repo: HelmRepo
constructor(repo: HelmRepo){
this.cache = HelmRepoManager.cache
this.repo = repo
this.cache = HelmRepoManager.cache;
this.repo = repo;
}
public async chart(name: string) {
const charts = await this.charts()
return charts[name]
const charts = await this.charts();
return charts[name];
}
public async charts(): Promise<any> {
try {
const cachedYaml = await this.cachedYaml()
return cachedYaml["entries"]
const cachedYaml = await this.cachedYaml();
return cachedYaml["entries"];
} catch(error) {
logger.error(error)
return []
logger.error(error);
return [];
}
}
public async getReadme(name: string, version = "") {
const helm = await helmCli.binaryPath()
const helm = await helmCli.binaryPath();
if(version && version != "") {
const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr)})
return stdout
const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
return stdout;
} else {
const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr)})
return stdout
const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
return stdout;
}
}
public async getValues(name: string, version = "") {
const helm = await helmCli.binaryPath()
const helm = await helmCli.binaryPath();
if(version && version != "") {
const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr)})
const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
return stdout
return stdout;
} else {
const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr)})
const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
return stdout
return stdout;
}
}
protected async cachedYaml(): Promise<CachedYaml> {
if (!(this.repo.name in this.cache)) {
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, 'utf-8')
const data = yaml.safeLoad(cacheFile)
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, 'utf-8');
const data = yaml.safeLoad(cacheFile);
for(const key in data["entries"]) {
data["entries"][key].forEach((version: any) => {
version['repo'] = this.repo.name
version['created'] = Date.parse(version.created).toString()
})
version['repo'] = this.repo.name;
version['created'] = Date.parse(version.created).toString();
});
}
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data))
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
}
return JSON.parse(this.cache[this.repo.name].toString())
return JSON.parse(this.cache[this.repo.name].toString());
}
}
+9 -9
View File
@@ -1,6 +1,6 @@
import packageInfo from "../../../package.json"
import path from "path"
import { LensBinary, LensBinaryOpts } from "../lens-binary"
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 {
@@ -11,24 +11,24 @@ export class HelmCli extends LensBinary {
baseDir: baseDir,
originalBinaryName: "helm",
newBinaryName: "helm3"
}
super(opts)
};
super(opts);
}
protected getTarName(): string | null {
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`;
}
protected getUrl() {
return `https://get.helm.sh/helm-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
return `https://get.helm.sh/helm-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`;
}
protected getBinaryPath() {
return path.join(this.dirname, this.binaryName)
return path.join(this.dirname, this.binaryName);
}
protected getOriginalBinaryPath() {
return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName)
return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName);
}
}
+48 -48
View File
@@ -1,7 +1,7 @@
import * as tempy from "tempy";
import fs from "fs";
import * as yaml from "js-yaml";
import { promiseExec} from "../promise-exec"
import { promiseExec} from "../promise-exec";
import { helmCli } from "./helm-cli";
import { Cluster } from "../cluster";
import { toCamelCase } from "../../common/utils/camelCase";
@@ -9,103 +9,103 @@ 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 } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
const helm = await helmCli.binaryPath();
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
const output = JSON.parse(stdout)
const output = JSON.parse(stdout);
if (output.length == 0) {
return output
return output;
}
output.forEach((release: any, index: number) => {
output[index] = toCamelCase(release)
output[index] = toCamelCase(release);
});
return output
return output;
}
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
const helm = await helmCli.binaryPath()
const fileName = tempy.file({name: "values.yaml"})
await fs.promises.writeFile(fileName, yaml.safeDump(values))
const helm = await helmCli.binaryPath();
const fileName = tempy.file({name: "values.yaml"});
await fs.promises.writeFile(fileName, yaml.safeDump(values));
try {
let generateName = ""
let generateName = "";
if (!name) {
generateName = "--generate-name"
name = ""
generateName = "--generate-name";
name = "";
}
const { stdout, stderr } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr)})
const releaseName = stdout.split("\n")[0].split(' ')[1].trim()
const { stdout, stderr } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
const releaseName = stdout.split("\n")[0].split(' ')[1].trim();
return {
log: stdout,
release: {
name: releaseName,
namespace: namespace
}
}
};
} finally {
await fs.promises.unlink(fileName)
await fs.promises.unlink(fileName);
}
}
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
const helm = await helmCli.binaryPath()
const fileName = tempy.file({name: "values.yaml"})
await fs.promises.writeFile(fileName, yaml.safeDump(values))
const helm = await helmCli.binaryPath();
const fileName = tempy.file({name: "values.yaml"});
await fs.promises.writeFile(fileName, yaml.safeDump(values));
try {
const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)})
const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
return {
log: stdout,
release: this.getRelease(name, namespace, cluster)
}
};
} finally {
await fs.promises.unlink(fileName)
await fs.promises.unlink(fileName);
}
}
public async getRelease(name: string, namespace: string, cluster: Cluster) {
const helm = await helmCli.binaryPath()
const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)})
const release = JSON.parse(stdout)
release.resources = await this.getResources(name, namespace, cluster)
return release
const helm = await helmCli.binaryPath();
const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
const release = JSON.parse(stdout);
release.resources = await this.getResources(name, namespace, cluster);
return release;
}
public async deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const { stdout, stderr } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
const helm = await helmCli.binaryPath();
const { stdout, stderr } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return stdout
return stdout;
}
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const { stdout, stderr } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
return stdout
const helm = await helmCli.binaryPath();
const { stdout, stderr } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return stdout;
}
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const {stdout, stderr} = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
return JSON.parse(stdout)
const helm = await helmCli.binaryPath();
const {stdout, stderr} = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return JSON.parse(stdout);
}
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const {stdout, stderr} = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
return stdout
const helm = await helmCli.binaryPath();
const {stdout, stderr} = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return stdout;
}
protected async getResources(name: string, namespace: string, cluster: Cluster) {
const helm = await helmCli.binaryPath()
const kubectl = await cluster.kubeCtl.getPath()
const pathToKubeconfig = cluster.getProxyKubeconfigPath()
const helm = await helmCli.binaryPath();
const kubectl = await cluster.kubeCtl.getPath();
const pathToKubeconfig = cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => {
return { stdout: JSON.stringify({items: []})}
})
return stdout
return { stdout: JSON.stringify({items: []})};
});
return stdout;
}
}
export const releaseManager = new HelmReleaseManager()
export const releaseManager = new HelmReleaseManager();
+30 -30
View File
@@ -46,34 +46,34 @@ export class HelmRepoManager extends Singleton {
}
async init() {
helmCli.setLogger(logger)
helmCli.setLogger(logger);
await helmCli.ensureBinary();
if (!this.initialized) {
this.helmEnv = await this.parseHelmEnv()
await this.update()
this.initialized = true
this.helmEnv = await this.parseHelmEnv();
await this.update();
this.initialized = true;
}
}
protected async parseHelmEnv() {
const helm = await helmCli.binaryPath()
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => {
throw(error.stderr)
})
const lines = stdout.split(/\r?\n/) // split by new line feed
const env: HelmEnv = {}
throw(error.stderr);
});
const lines = stdout.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=")
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, "") // strip quotas
env[key] = value.replace(/"/g, ""); // strip quotas
}
})
return env
});
return env;
}
public async repositories(): Promise<HelmRepo[]> {
if (!this.initialized) {
await this.init()
await this.init();
}
try {
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
@@ -91,41 +91,41 @@ export class HelmRepoManager extends Singleton {
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error "${error}"`)
return []
logger.error(`[HELM]: repositories listing error "${error}"`);
return [];
}
}
public async repository(name: string) {
const repositories = await this.repositories()
const repositories = await this.repositories();
return repositories.find(repo => repo.name == name);
}
public async update() {
const helm = await helmCli.binaryPath()
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
return { stdout: error.stdout }
})
return stdout
return { stdout: error.stdout };
});
return stdout;
}
public async addRepo({ name, url }: HelmRepo) {
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
const helm = await helmCli.binaryPath()
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
throw(error.stderr)
})
return stdout
throw(error.stderr);
});
return stdout;
}
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo "${name}" from ${url}`);
const helm = await helmCli.binaryPath()
const helm = await helmCli.binaryPath();
const { stdout, stderr } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
throw(error.stderr)
})
return stdout
throw(error.stderr);
});
return stdout;
}
}
export const repoManager = HelmRepoManager.getInstance<HelmRepoManager>()
export const repoManager = HelmRepoManager.getInstance<HelmRepoManager>();
+41 -41
View File
@@ -6,93 +6,93 @@ import { releaseManager } from "./helm-release-manager";
class HelmService {
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath())
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath());
}
public async listCharts() {
const charts: any = {}
await repoManager.init()
const repositories = await repoManager.repositories()
const charts: any = {};
await repoManager.init();
const repositories = await repoManager.repositories();
for (const repo of repositories) {
charts[repo.name] = {}
const manager = new HelmChartManager(repo)
let entries = await manager.charts()
entries = this.excludeDeprecated(entries)
charts[repo.name] = {};
const manager = new HelmChartManager(repo);
let entries = await manager.charts();
entries = this.excludeDeprecated(entries);
for (const key in entries) {
entries[key] = entries[key][0]
entries[key] = entries[key][0];
}
charts[repo.name] = entries
charts[repo.name] = entries;
}
return charts
return charts;
}
public async getChart(repoName: string, chartName: string, version = "") {
const result = {
readme: "",
versions: {}
}
const repo = await repoManager.repository(repoName)
const chartManager = new HelmChartManager(repo)
const chart = await chartManager.chart(chartName)
result.readme = await chartManager.getReadme(chartName, version)
result.versions = chart
return result
};
const repo = await repoManager.repository(repoName);
const chartManager = new HelmChartManager(repo);
const chart = await chartManager.chart(chartName);
result.readme = await chartManager.getReadme(chartName, version);
result.versions = chart;
return result;
}
public async getChartValues(repoName: string, chartName: string, version = "") {
const repo = await repoManager.repository(repoName)
const chartManager = new HelmChartManager(repo)
return chartManager.getValues(chartName, version)
const repo = await repoManager.repository(repoName);
const chartManager = new HelmChartManager(repo);
return chartManager.getValues(chartName, version);
}
public async listReleases(cluster: Cluster, namespace: string = null) {
await repoManager.init()
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace)
await repoManager.init();
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
}
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release")
return await releaseManager.getRelease(releaseName, namespace, cluster)
logger.debug("Fetch release");
return await releaseManager.getRelease(releaseName, namespace, cluster);
}
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release values")
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath())
logger.debug("Fetch release values");
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
}
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release history")
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath())
logger.debug("Fetch release history");
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
}
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Delete release")
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath())
logger.debug("Delete release");
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
}
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
logger.debug("Upgrade release")
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster)
logger.debug("Upgrade release");
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
}
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
logger.debug("Rollback release")
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath())
return { message: output }
logger.debug("Rollback release");
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
return { message: output };
}
protected excludeDeprecated(entries: any) {
for (const key in entries) {
entries[key] = entries[key].filter((entry: any) => {
if (Array.isArray(entry)) {
return entry[0]['deprecated'] != true
return entry[0]['deprecated'] != true;
}
return entry["deprecated"] != true
})
return entry["deprecated"] != true;
});
}
return entries
return entries;
}
}
export const helmService = new HelmService()
export const helmService = new HelmService();
+29 -29
View File
@@ -1,25 +1,25 @@
// Main process
import "../common/system-ca"
import "../common/prometheus-providers"
import * as Mobx from "mobx"
import "../common/system-ca";
import "../common/prometheus-providers";
import * as Mobx from "mobx";
import * as LensExtensions from "../extensions/core-api";
import { app, dialog } from "electron"
import { app, dialog } from "electron";
import { appName } from "../common/vars";
import path from "path"
import { LensProxy } from "./lens-proxy"
import path from "path";
import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
import { AppUpdater } from "./app-updater"
import { shellSync } from "./shell-sync"
import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env"
import { AppUpdater } from "./app-updater";
import { shellSync } from "./shell-sync";
import { getFreePort } from "./port";
import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger"
import { clusterStore } from "../common/cluster-store"
import logger from "./logger";
import { clusterStore } from "../common/cluster-store";
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { appEventBus } from "../common/event-bus"
import { appEventBus } from "../common/event-bus";
import { extensionLoader } from "../extensions/extension-loader";
import { extensionManager } from "../extensions/extension-manager";
import { extensionsStore } from "../extensions/extensions-store";
@@ -35,16 +35,16 @@ if (!process.env.CICD) {
app.setPath("userData", workingDir);
}
mangleProxyEnv()
mangleProxyEnv();
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
}
app.on("ready", async () => {
logger.info(`🚀 Starting Lens from "${workingDir}"`)
logger.info(`🚀 Starting Lens from "${workingDir}"`);
await shellSync();
const updater = new AppUpdater()
const updater = new AppUpdater();
updater.start();
registerFileProtocol("static", __static);
@@ -59,10 +59,10 @@ app.on("ready", async () => {
// find free port
try {
proxyPort = await getFreePort()
proxyPort = await getFreePort();
} catch (error) {
logger.error(error)
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
logger.error(error);
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy");
app.exit();
}
@@ -73,22 +73,22 @@ app.on("ready", async () => {
try {
proxyServer = LensProxy.create(proxyPort, clusterManager);
} catch (error) {
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`)
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`)
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`);
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`);
app.exit();
}
extensionLoader.init()
extensionLoader.init();
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
extensionLoader.initExtensions(await extensionManager.load()); // call after windowManager to see splash earlier
setTimeout(() => {
appEventBus.emit({ name: "service", action: "start" })
}, 1000)
appEventBus.emit({ name: "service", action: "start" });
}, 1000);
});
app.on("activate", (event, hasVisibleWindows) => {
logger.info('APP:ACTIVATE', { hasVisibleWindows })
logger.info('APP:ACTIVATE', { hasVisibleWindows });
if (!hasVisibleWindows) {
windowManager.initMainWindow();
}
@@ -97,11 +97,11 @@ app.on("activate", (event, hasVisibleWindows) => {
// Quit app on Cmd+Q (MacOS)
app.on("will-quit", (event) => {
logger.info('APP:QUIT');
appEventBus.emit({name: "app", action: "close"})
appEventBus.emit({name: "app", action: "close"});
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
clusterManager?.stop(); // close cluster connections
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
})
});
// Extensions-api runtime exports
export const LensExtensionsApi = {
@@ -111,4 +111,4 @@ export const LensExtensionsApi = {
export {
Mobx,
LensExtensionsApi as LensExtensions,
}
};
+38 -38
View File
@@ -1,10 +1,10 @@
import { ChildProcess, spawn } from "child_process"
import { ChildProcess, spawn } from "child_process";
import { waitUntilUsed } from "tcp-port-used";
import { broadcastMessage } from "../common/ipc";
import type { Cluster } from "./cluster"
import { Kubectl } from "./kubectl"
import logger from "./logger"
import * as url from "url"
import type { Cluster } from "./cluster";
import { Kubectl } from "./kubectl";
import logger from "./logger";
import * as url from "url";
export interface KubeAuthProxyLog {
data: string;
@@ -21,10 +21,10 @@ export class KubeAuthProxy {
protected kubectl: Kubectl
constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) {
this.env = env
this.port = port
this.cluster = cluster
this.kubectl = Kubectl.bundled()
this.env = env;
this.port = port;
this.cluster = cluster;
this.kubectl = Kubectl.bundled();
}
get acceptHosts() {
@@ -36,7 +36,7 @@ export class KubeAuthProxy {
return;
}
const proxyBin = await this.kubectl.getPath()
const proxyBin = await this.kubectl.getPath();
const args = [
"proxy",
"-p", `${this.port}`,
@@ -44,63 +44,63 @@ export class KubeAuthProxy {
"--context", `${this.cluster.contextName}`,
"--accept-hosts", this.acceptHosts,
"--reject-paths", "^[^/]"
]
];
if (process.env.DEBUG_PROXY === "true") {
args.push("-v", "9")
args.push("-v", "9");
}
logger.debug(`spawning kubectl proxy with args: ${args}`)
this.proxyProcess = spawn(proxyBin, args, { env: this.env, })
logger.debug(`spawning kubectl proxy with args: ${args}`);
this.proxyProcess = spawn(proxyBin, args, { env: this.env, });
this.proxyProcess.on("error", (error) => {
this.sendIpcLogMessage({ data: error.message, error: true })
this.exit()
})
this.sendIpcLogMessage({ data: error.message, error: true });
this.exit();
});
this.proxyProcess.on("exit", (code) => {
this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 })
this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 });
this.exit();
})
});
this.proxyProcess.stdout.on('data', (data) => {
let logItem = data.toString()
let logItem = data.toString();
if (logItem.startsWith("Starting to serve on")) {
logItem = "Authentication proxy started\n"
logItem = "Authentication proxy started\n";
}
this.sendIpcLogMessage({ data: logItem })
})
this.sendIpcLogMessage({ data: logItem });
});
this.proxyProcess.stderr.on('data', (data) => {
this.lastError = this.parseError(data.toString())
this.sendIpcLogMessage({ data: data.toString(), error: true })
})
this.lastError = this.parseError(data.toString());
this.sendIpcLogMessage({ data: data.toString(), error: true });
});
return waitUntilUsed(this.port, 500, 10000)
return waitUntilUsed(this.port, 500, 10000);
}
protected parseError(data: string) {
const error = data.split("http: proxy error:").slice(1).join("").trim()
let errorMsg = error
const jsonError = error.split("Response: ")[1]
const error = data.split("http: proxy error:").slice(1).join("").trim();
let errorMsg = error;
const jsonError = error.split("Response: ")[1];
if (jsonError) {
try {
const parsedError = JSON.parse(jsonError)
errorMsg = parsedError.error_description || parsedError.error || jsonError
const parsedError = JSON.parse(jsonError);
errorMsg = parsedError.error_description || parsedError.error || jsonError;
} catch (_) {
errorMsg = jsonError.trim()
errorMsg = jsonError.trim();
}
}
return errorMsg
return errorMsg;
}
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
const channel = `kube-auth:${this.cluster.id}`
const channel = `kube-auth:${this.cluster.id}`;
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastMessage(channel, res)
broadcastMessage(channel, res);
}
public exit() {
if (!this.proxyProcess) return;
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta())
this.proxyProcess.kill()
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
this.proxyProcess.kill();
this.proxyProcess.removeAllListeners();
this.proxyProcess.stderr.removeAllListeners();
this.proxyProcess.stdout.removeAllListeners();
+15 -15
View File
@@ -1,11 +1,11 @@
import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "./cluster"
import type { Cluster } from "./cluster";
import type { ContextHandler } from "./context-handler";
import { app } from "electron"
import path from "path"
import fs from "fs-extra"
import { dumpConfigYaml, loadConfig } from "../common/kube-helpers"
import logger from "./logger"
import { app } from "electron";
import path from "path";
import fs from "fs-extra";
import { dumpConfigYaml, loadConfig } from "../common/kube-helpers";
import logger from "./logger";
export class KubeconfigManager {
protected configDir = app.getPath("temp")
@@ -14,9 +14,9 @@ export class KubeconfigManager {
private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
const kcm = new KubeconfigManager(cluster, contextHandler, port)
await kcm.init()
return kcm
const kcm = new KubeconfigManager(cluster, contextHandler, port);
await kcm.init();
return kcm;
}
protected async init() {
@@ -24,7 +24,7 @@ export class KubeconfigManager {
await this.contextHandler.ensurePort();
await this.createProxyKubeconfig();
} catch (err) {
logger.error(`Failed to created temp config for auth-proxy`, { err })
logger.error(`Failed to created temp config for auth-proxy`, { err });
}
}
@@ -33,7 +33,7 @@ export class KubeconfigManager {
}
protected resolveProxyUrl() {
return `http://127.0.0.1:${this.port}/${this.cluster.id}`
return `http://127.0.0.1:${this.port}/${this.cluster.id}`;
}
/**
@@ -78,11 +78,11 @@ export class KubeconfigManager {
async unlink() {
if (!this.tempFile) {
return
return;
}
logger.info('Deleting temporary kubeconfig: ' + this.tempFile)
await fs.unlink(this.tempFile)
this.tempFile = undefined
logger.info('Deleting temporary kubeconfig: ' + this.tempFile);
await fs.unlink(this.tempFile);
this.tempFile = undefined;
}
}
+163 -163
View File
@@ -1,17 +1,17 @@
import { app, remote } from "electron"
import path from "path"
import fs from "fs"
import { promiseExec } from "./promise-exec"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import * as lockFile from "proper-lockfile"
import { helmCli } from "./helm/helm-cli"
import { userStore } from "../common/user-store"
import { app, remote } from "electron";
import path from "path";
import fs from "fs";
import { promiseExec } from "./promise-exec";
import logger from "./logger";
import { ensureDir, pathExists } from "fs-extra";
import * as lockFile from "proper-lockfile";
import { helmCli } from "./helm/helm-cli";
import { userStore } from "../common/user-store";
import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version"
import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
const bundledVersion = getBundledKubectlVersion()
const bundledVersion = getBundledKubectlVersion();
const kubectlMap: Map<string, string> = new Map([
["1.7", "1.8.15"],
["1.8", "1.9.10"],
@@ -26,31 +26,31 @@ const kubectlMap: Map<string, string> = new Map([
["1.17", bundledVersion],
["1.18", "1.18.8"],
["1.19", "1.19.0"]
])
]);
const packageMirrors: Map<string, string> = new Map([
["default", "https://storage.googleapis.com/kubernetes-release/release"],
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
])
]);
let bundledPath: string
const initScriptVersionString = "# lens-initscript v3\n"
let bundledPath: string;
const initScriptVersionString = "# lens-initscript v3\n";
export function bundledKubectlPath(): string {
if (bundledPath) { return bundledPath }
if (bundledPath) { return bundledPath; }
if (isDevelopment || isTestEnv) {
const platformName = isWindows ? "windows" : process.platform
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl")
const platformName = isWindows ? "windows" : process.platform;
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl");
} else {
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl")
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl");
}
if (isWindows) {
bundledPath = `${bundledPath}.exe`
bundledPath = `${bundledPath}.exe`;
}
return bundledPath
return bundledPath;
}
export class Kubectl {
@@ -61,7 +61,7 @@ export class Kubectl {
protected dirname: string
static get kubectlDir() {
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl");
}
public static readonly bundledKubectlVersion: string = bundledVersion
@@ -70,270 +70,270 @@ export class Kubectl {
// Returns the single bundled Kubectl instance
public static bundled() {
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion)
return Kubectl.bundledInstance
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
return Kubectl.bundledInstance;
}
constructor(clusterVersion: string) {
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion)
const minorVersion = versionParts[1]
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
const minorVersion = versionParts[1];
/* minorVersion is the first two digits of kube server version
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
if (kubectlMap.has(minorVersion)) {
this.kubectlVersion = kubectlMap.get(minorVersion)
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using version map")
this.kubectlVersion = kubectlMap.get(minorVersion);
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using version map");
} else {
this.kubectlVersion = versionParts[1] + versionParts[2]
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using fallback")
this.kubectlVersion = versionParts[1] + versionParts[2];
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using fallback");
}
let arch = null
let arch = null;
if (process.arch == "x64") {
arch = "amd64"
arch = "amd64";
} else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386"
arch = "386";
} else {
arch = process.arch
arch = process.arch;
}
const platformName = isWindows ? "windows" : process.platform
const binaryName = isWindows ? "kubectl.exe" : "kubectl"
const platformName = isWindows ? "windows" : process.platform;
const binaryName = isWindows ? "kubectl.exe" : "kubectl";
this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`
this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`;
this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion))
this.path = path.join(this.dirname, binaryName)
this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion));
this.path = path.join(this.dirname, binaryName);
}
public getBundledPath() {
return bundledKubectlPath()
return bundledKubectlPath();
}
public getPathFromPreferences() {
return userStore.preferences?.kubectlBinariesPath || this.getBundledPath()
return userStore.preferences?.kubectlBinariesPath || this.getBundledPath();
}
protected getDownloadDir() {
if (userStore.preferences?.downloadBinariesPath) {
return path.join(userStore.preferences.downloadBinariesPath, "kubectl")
return path.join(userStore.preferences.downloadBinariesPath, "kubectl");
}
return Kubectl.kubectlDir
return Kubectl.kubectlDir;
}
public async getPath(bundled = false): Promise<string> {
if (userStore.preferences?.downloadKubectlBinaries === false) {
return this.getPathFromPreferences()
return this.getPathFromPreferences();
}
// return binary name if bundled path is not functional
if (!await this.checkBinary(this.getBundledPath(), false)) {
Kubectl.invalidBundle = true
return path.basename(this.getBundledPath())
Kubectl.invalidBundle = true;
return path.basename(this.getBundledPath());
}
try {
if (!await this.ensureKubectl()) {
logger.error("Failed to ensure kubectl, fallback to the bundled version")
return this.getBundledPath()
logger.error("Failed to ensure kubectl, fallback to the bundled version");
return this.getBundledPath();
}
return this.path
return this.path;
} catch (err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version")
logger.error(err)
return this.getBundledPath()
logger.error("Failed to ensure kubectl, fallback to the bundled version");
logger.error(err);
return this.getBundledPath();
}
}
public async binDir() {
try {
await this.ensureKubectl()
await this.writeInitScripts()
return this.dirname
await this.ensureKubectl();
await this.writeInitScripts();
return this.dirname;
} catch (err) {
logger.error(err)
return ""
logger.error(err);
return "";
}
}
public async checkBinary(path: string, checkVersion = true) {
const exists = await pathExists(path)
const exists = await pathExists(path);
if (exists) {
try {
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`)
const output = JSON.parse(stdout)
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
const output = JSON.parse(stdout);
if (!checkVersion) {
return true
return true;
}
let version: string = output.clientVersion.gitVersion
let version: string = output.clientVersion.gitVersion;
if (version[0] === 'v') {
version = version.slice(1)
version = version.slice(1);
}
if (version === this.kubectlVersion) {
logger.debug(`Local kubectl is version ${this.kubectlVersion}`)
return true
logger.debug(`Local kubectl is version ${this.kubectlVersion}`);
return true;
}
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`)
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
} catch (err) {
logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`)
logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`);
}
await fs.promises.unlink(this.path)
await fs.promises.unlink(this.path);
}
return false
return false;
}
protected async checkBundled(): Promise<boolean> {
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
try {
const exist = await pathExists(this.path)
const exist = await pathExists(this.path);
if (!exist) {
await fs.promises.copyFile(this.getBundledPath(), this.path)
await fs.promises.chmod(this.path, 0o755)
await fs.promises.copyFile(this.getBundledPath(), this.path);
await fs.promises.chmod(this.path, 0o755);
}
return true
return true;
} catch (err) {
logger.error("Could not copy the bundled kubectl to app-data: " + err)
return false
logger.error("Could not copy the bundled kubectl to app-data: " + err);
return false;
}
} else {
return false
return false;
}
}
public async ensureKubectl(): Promise<boolean> {
if (userStore.preferences?.downloadKubectlBinaries === false) {
return true
return true;
}
if (Kubectl.invalidBundle) {
logger.error(`Detected invalid bundle binary, returning ...`)
return false
logger.error(`Detected invalid bundle binary, returning ...`);
return false;
}
await ensureDir(this.dirname, 0o755)
await ensureDir(this.dirname, 0o755);
return lockFile.lock(this.dirname).then(async (release) => {
logger.debug(`Acquired a lock for ${this.kubectlVersion}`)
const bundled = await this.checkBundled()
let isValid = await this.checkBinary(this.path, !bundled)
logger.debug(`Acquired a lock for ${this.kubectlVersion}`);
const bundled = await this.checkBundled();
let isValid = await this.checkBinary(this.path, !bundled);
if (!isValid && !bundled) {
await this.downloadKubectl().catch((error) => {
logger.error(error)
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release()
return false
logger.error(error);
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
release();
return false;
});
isValid = !await this.checkBinary(this.path, false)
isValid = !await this.checkBinary(this.path, false);
}
if (!isValid) {
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release()
return false
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
release();
return false;
}
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release()
return true
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
release();
return true;
}).catch((e) => {
logger.error(`Failed to get a lock for ${this.kubectlVersion}`)
logger.error(e)
return false
})
logger.error(`Failed to get a lock for ${this.kubectlVersion}`);
logger.error(e);
return false;
});
}
public async downloadKubectl() {
await ensureDir(path.dirname(this.path), 0o755)
await ensureDir(path.dirname(this.path), 0o755);
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`)
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
return new Promise((resolve, reject) => {
const stream = customRequest({
url: this.url,
gzip: true,
});
const file = fs.createWriteStream(this.path)
const file = fs.createWriteStream(this.path);
stream.on("complete", () => {
logger.debug("kubectl binary download finished")
file.end()
})
logger.debug("kubectl binary download finished");
file.end();
});
stream.on("error", (error) => {
logger.error(error)
logger.error(error);
fs.unlink(this.path, () => {
// do nothing
})
reject(error)
})
});
reject(error);
});
file.on("close", () => {
logger.debug("kubectl binary download closed")
logger.debug("kubectl binary download closed");
fs.chmod(this.path, 0o755, (err) => {
if (err) reject(err);
})
resolve()
})
stream.pipe(file)
})
});
resolve();
});
stream.pipe(file);
});
}
protected async writeInitScripts() {
const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences())
const helmPath = helmCli.getBinaryDir()
const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
const helmPath = helmCli.getBinaryDir();
const fsPromises = fs.promises;
const bashScriptPath = path.join(this.dirname, '.bash_set_path')
const bashScriptPath = path.join(this.dirname, '.bash_set_path');
let bashScript = "" + initScriptVersionString
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n"
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n"
bashScript += " . \"$HOME/.bash_profile\"\n"
bashScript += "elif test -f \"$HOME/.bash_login\"; then\n"
bashScript += " . \"$HOME/.bash_login\"\n"
bashScript += "elif test -f \"$HOME/.profile\"; then\n"
bashScript += " . \"$HOME/.profile\"\n"
bashScript += "fi\n"
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
let bashScript = "" + initScriptVersionString;
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
bashScript += " . \"$HOME/.bash_profile\"\n";
bashScript += "elif test -f \"$HOME/.bash_login\"; then\n";
bashScript += " . \"$HOME/.bash_login\"\n";
bashScript += "elif test -f \"$HOME/.profile\"; then\n";
bashScript += " . \"$HOME/.profile\"\n";
bashScript += "fi\n";
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
bashScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
bashScript += "export NO_PROXY\n"
bashScript += "unset tempkubeconfig\n"
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 })
bashScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n";
bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n";
bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n";
bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n";
bashScript += "export NO_PROXY\n";
bashScript += "unset tempkubeconfig\n";
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
const zshScriptPath = path.join(this.dirname, '.zlogin')
const zshScriptPath = path.join(this.dirname, '.zlogin');
let zshScript = "" + initScriptVersionString
let zshScript = "" + initScriptVersionString;
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
// restore previous ZDOTDIR
zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n"
zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n";
// source all the files
zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n"
zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n"
zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n"
zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n"
zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n";
zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n";
zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n";
zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n";
// voodoo to replace any previous occurrences of kubectl path in the PATH
zshScript += `kubectlpath=\"${kubectlPath}"\n`
zshScript += `helmpath=\"${helmPath}"\n`
zshScript += "p=\":$kubectlpath:\"\n"
zshScript += "d=\":$PATH:\"\n"
zshScript += "d=${d//$p/:}\n"
zshScript += "d=${d/#:/}\n"
zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n"
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
zshScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
zshScript += "export NO_PROXY\n"
zshScript += "unset tempkubeconfig\n"
zshScript += "unset OLD_ZDOTDIR\n"
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 })
zshScript += `kubectlpath=\"${kubectlPath}"\n`;
zshScript += `helmpath=\"${helmPath}"\n`;
zshScript += "p=\":$kubectlpath:\"\n";
zshScript += "d=\":$PATH:\"\n";
zshScript += "d=${d//$p/:}\n";
zshScript += "d=${d/#:/}\n";
zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n";
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
zshScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n";
zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n";
zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n";
zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n";
zshScript += "export NO_PROXY\n";
zshScript += "unset tempkubeconfig\n";
zshScript += "unset OLD_ZDOTDIR\n";
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
}
protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror)
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
if (mirror) {
return mirror
return mirror;
}
return packageMirrors.get("default") // MacOS packages are only available from default
return packageMirrors.get("default"); // MacOS packages are only available from default
}
}
+21 -21
View File
@@ -1,5 +1,5 @@
import packageInfo from "../../package.json"
import path from "path"
import packageInfo from "../../package.json";
import path from "path";
import { Kubectl } from "../../src/main/kubectl";
import { isWindows } from "../common/vars";
@@ -7,39 +7,39 @@ jest.mock("../common/user-store");
describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion)
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
})
const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion);
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
});
it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
})
})
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
});
});
describe("getPath()", () => {
it("returns path to downloaded kubectl binary", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
const kubectlPath = await kubectl.getPath()
let binaryName = "kubectl"
const kubectlPath = await kubectl.getPath();
let binaryName = "kubectl";
if (isWindows) {
binaryName += ".exe"
binaryName += ".exe";
}
const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName)
expect(kubectlPath).toBe(expectedPath)
})
const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName);
expect(kubectlPath).toBe(expectedPath);
});
it("returns plain binary name if bundled kubectl is non-functional", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl")
const kubectlPath = await kubectl.getPath()
let binaryName = "kubectl"
jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl");
const kubectlPath = await kubectl.getPath();
let binaryName = "kubectl";
if (isWindows) {
binaryName += ".exe"
binaryName += ".exe";
}
expect(kubectlPath).toBe(binaryName)
})
})
expect(kubectlPath).toBe(binaryName);
});
});
+5 -5
View File
@@ -2,16 +2,16 @@ import http from "http";
export abstract class LensApi {
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
this.respond(res, JSON.stringify(content), "application/json", status)
this.respond(res, JSON.stringify(content), "application/json", status);
}
protected respondText(res: http.ServerResponse, content: string, status = 200) {
this.respond(res, content, "text/plain", status)
this.respond(res, content, "text/plain", status);
}
protected respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
res.setHeader("Content-Type", contentType)
res.statusCode = status
res.end(content)
res.setHeader("Content-Type", contentType);
res.statusCode = status;
res.end(content);
}
}
+72 -72
View File
@@ -1,10 +1,10 @@
import path from "path"
import fs from "fs"
import request from "request"
import { ensureDir, pathExists } from "fs-extra"
import * as tar from "tar"
import path from "path";
import fs from "fs";
import request from "request";
import { ensureDir, pathExists } from "fs-extra";
import * as tar from "tar";
import { isWindows } from "../common/vars";
import winston from "winston"
import winston from "winston";
export type LensBinaryOpts = {
version: string;
@@ -30,159 +30,159 @@ export class LensBinary {
protected logger: Console | winston.Logger
constructor(opts: LensBinaryOpts) {
const baseDir = opts.baseDir
this.originalBinaryName = opts.originalBinaryName
this.binaryName = opts.newBinaryName || opts.originalBinaryName
this.binaryVersion = opts.version
this.requestOpts = opts.requestOpts
this.logger = console
let arch = null
const baseDir = opts.baseDir;
this.originalBinaryName = opts.originalBinaryName;
this.binaryName = opts.newBinaryName || opts.originalBinaryName;
this.binaryVersion = opts.version;
this.requestOpts = opts.requestOpts;
this.logger = console;
let arch = null;
if (process.arch == "x64") {
arch = "amd64"
arch = "amd64";
}
else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386"
arch = "386";
}
else {
arch = process.arch
arch = process.arch;
}
this.arch = arch
this.platformName = isWindows ? "windows" : process.platform
this.dirname = path.normalize(path.join(baseDir, this.binaryName))
this.arch = arch;
this.platformName = isWindows ? "windows" : process.platform;
this.dirname = path.normalize(path.join(baseDir, this.binaryName));
if (isWindows) {
this.binaryName = this.binaryName + ".exe"
this.originalBinaryName = this.originalBinaryName + ".exe"
this.binaryName = this.binaryName + ".exe";
this.originalBinaryName = this.originalBinaryName + ".exe";
}
const tarName = this.getTarName()
const tarName = this.getTarName();
if (tarName) {
this.tarPath = path.join(this.dirname, tarName)
this.tarPath = path.join(this.dirname, tarName);
}
}
public setLogger(logger: Console | winston.Logger) {
this.logger = logger
this.logger = logger;
}
protected binaryDir() {
throw new Error("binaryDir not implemented")
throw new Error("binaryDir not implemented");
}
public async binaryPath() {
await this.ensureBinary()
return this.getBinaryPath()
await this.ensureBinary();
return this.getBinaryPath();
}
protected getTarName(): string | null {
return null
return null;
}
protected getUrl() {
return ""
return "";
}
protected getBinaryPath() {
return ""
return "";
}
protected getOriginalBinaryPath() {
return ""
return "";
}
public getBinaryDir() {
return path.dirname(this.getBinaryPath())
return path.dirname(this.getBinaryPath());
}
public async binDir() {
try {
await this.ensureBinary()
return this.dirname
await this.ensureBinary();
return this.dirname;
} catch (err) {
this.logger.error(err)
return ""
this.logger.error(err);
return "";
}
}
protected async checkBinary() {
const exists = await pathExists(this.getBinaryPath())
return exists
const exists = await pathExists(this.getBinaryPath());
return exists;
}
public async ensureBinary() {
const isValid = await this.checkBinary()
const isValid = await this.checkBinary();
if (!isValid) {
await this.downloadBinary().catch((error) => {
this.logger.error(error)
this.logger.error(error);
});
if (this.tarPath) await this.untarBinary()
if (this.originalBinaryName != this.binaryName) await this.renameBinary()
this.logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
if (this.tarPath) await this.untarBinary();
if (this.originalBinaryName != this.binaryName) await this.renameBinary();
this.logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`);
}
}
protected async untarBinary() {
return new Promise<void>((resolve, reject) => {
this.logger.debug(`Extracting ${this.originalBinaryName} binary`)
this.logger.debug(`Extracting ${this.originalBinaryName} binary`);
tar.x({
file: this.tarPath,
cwd: this.dirname
}).then((_ => {
resolve()
}))
})
resolve();
}));
});
}
protected async renameBinary() {
return new Promise<void>((resolve, reject) => {
this.logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`)
this.logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`);
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
if (err) {
reject(err)
reject(err);
}
else {
resolve()
resolve();
}
})
})
});
});
}
protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath()
await ensureDir(this.getBinaryDir(), 0o755)
const binaryPath = this.tarPath || this.getBinaryPath();
await ensureDir(this.getBinaryDir(), 0o755);
const file = fs.createWriteStream(binaryPath)
const url = this.getUrl()
const file = fs.createWriteStream(binaryPath);
const url = this.getUrl();
this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`)
this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`);
const requestOpts: request.UriOptions & request.CoreOptions = {
uri: url,
gzip: true,
...this.requestOpts
}
};
const stream = request(requestOpts)
const stream = request(requestOpts);
stream.on("complete", () => {
this.logger.info(`Download of ${this.originalBinaryName} finished`)
file.end()
})
this.logger.info(`Download of ${this.originalBinaryName} finished`);
file.end();
});
stream.on("error", (error) => {
this.logger.error(error)
this.logger.error(error);
fs.unlink(binaryPath, () => {
// do nothing
})
throw(error)
})
});
throw(error);
});
return new Promise((resolve, reject) => {
file.on("close", () => {
this.logger.debug(`${this.originalBinaryName} binary download closed`)
this.logger.debug(`${this.originalBinaryName} binary download closed`);
if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => {
if (err) reject(err);
})
resolve()
})
stream.pipe(file)
})
});
resolve();
});
stream.pipe(file);
});
}
}
+61 -61
View File
@@ -3,13 +3,13 @@ import http from "http";
import spdy from "spdy";
import httpProxy from "http-proxy";
import url from "url";
import * as WebSocket from "ws"
import { apiPrefix, apiKubePrefix } from "../common/vars"
import * as WebSocket from "ws";
import { apiPrefix, apiKubePrefix } from "../common/vars";
import { openShell } from "./node-shell-session";
import { Router } from "./router"
import { ClusterManager } from "./cluster-manager"
import { Router } from "./router";
import { ClusterManager } from "./cluster-manager";
import { ContextHandler } from "./context-handler";
import logger from "./logger"
import logger from "./logger";
export class LensProxy {
protected origin: string
@@ -23,7 +23,7 @@ export class LensProxy {
}
private constructor(protected port: number, protected clusterManager: ClusterManager) {
this.origin = `http://localhost:${port}`
this.origin = `http://localhost:${port}`;
this.router = new Router();
}
@@ -35,8 +35,8 @@ export class LensProxy {
close() {
logger.info("Closing proxy server");
this.proxyServer.close()
this.closed = true
this.proxyServer.close();
this.closed = true;
}
protected buildCustomProxy(): http.Server {
@@ -47,66 +47,66 @@ export class LensProxy {
protocols: ["http/1.1", "spdy/3.1"]
}
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res)
})
this.handleRequest(proxy, req, res);
});
spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
if (req.url.startsWith(`${apiPrefix}?`)) {
this.handleWsUpgrade(req, socket, head)
this.handleWsUpgrade(req, socket, head);
} else {
this.handleProxyUpgrade(proxy, req, socket, head)
this.handleProxyUpgrade(proxy, req, socket, head);
}
})
});
spdyProxy.on("error", (err) => {
logger.error("proxy error", err)
})
return spdyProxy
logger.error("proxy error", err);
});
return spdyProxy;
}
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const cluster = this.clusterManager.getClusterForRequest(req)
const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "")
const apiUrl = url.parse(cluster.apiUrl)
const pUrl = url.parse(proxyUrl)
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }
const proxySocket = new net.Socket()
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const apiUrl = url.parse(cluster.apiUrl);
const pUrl = url.parse(proxyUrl);
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
const proxySocket = new net.Socket();
proxySocket.connect(connectOpts, () => {
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`)
proxySocket.write(`Host: ${apiUrl.host}\r\n`)
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
for (let i = 0; i < req.rawHeaders.length; i += 2) {
const key = req.rawHeaders[i]
const key = req.rawHeaders[i];
if (key !== "Host" && key !== "Authorization") {
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`)
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
}
}
proxySocket.write("\r\n")
proxySocket.write(head)
})
proxySocket.write("\r\n");
proxySocket.write(head);
});
proxySocket.setKeepAlive(true)
socket.setKeepAlive(true)
proxySocket.setTimeout(0)
socket.setTimeout(0)
proxySocket.setKeepAlive(true);
socket.setKeepAlive(true);
proxySocket.setTimeout(0);
socket.setTimeout(0);
proxySocket.on('data', function (chunk) {
socket.write(chunk)
})
socket.write(chunk);
});
proxySocket.on('end', function () {
socket.end()
})
socket.end();
});
proxySocket.on('error', function (err) {
socket.write("HTTP/" + req.httpVersion + " 500 Connection error\r\n\r\n");
socket.end()
})
socket.end();
});
socket.on('data', function (chunk) {
proxySocket.write(chunk)
})
proxySocket.write(chunk);
});
socket.on('end', function () {
proxySocket.end()
})
proxySocket.end();
});
socket.on('error', function () {
proxySocket.end()
})
proxySocket.end();
});
}
}
@@ -120,29 +120,29 @@ export class LensProxy {
logger.debug("Failed proxy to target: " + JSON.stringify(target, null, 2));
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
const reqId = this.getRequestId(req);
const retryCount = this.retryCounters.get(reqId) || 0
const timeoutMs = retryCount * 250
const retryCount = this.retryCounters.get(reqId) || 0;
const timeoutMs = retryCount * 250;
if (retryCount < 20) {
logger.debug(`Retrying proxy request to url: ${reqId}`)
logger.debug(`Retrying proxy request to url: ${reqId}`);
setTimeout(() => {
this.retryCounters.set(reqId, retryCount + 1)
this.handleRequest(proxy, req, res)
}, timeoutMs)
this.retryCounters.set(reqId, retryCount + 1);
this.handleRequest(proxy, req, res);
}, timeoutMs);
}
}
}
try {
res.writeHead(500).end("Oops, something went wrong.")
res.writeHead(500).end("Oops, something went wrong.");
} catch (e) {
logger.error(`[LENS-PROXY]: Failed to write headers: `, e)
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
}
})
});
return proxy;
}
protected createWsListener(): WebSocket.Server {
const ws = new WebSocket.Server({ noServer: true })
const ws = new WebSocket.Server({ noServer: true });
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req);
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
@@ -152,10 +152,10 @@ export class LensProxy {
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
if (req.url.startsWith(apiKubePrefix)) {
delete req.headers.authorization
req.url = req.url.replace(apiKubePrefix, "")
const isWatchRequest = req.url.includes("watch=")
return await contextHandler.getApiTarget(isWatchRequest)
delete req.headers.authorization;
req.url = req.url.replace(apiKubePrefix, "");
const isWatchRequest = req.url.includes("watch=");
return await contextHandler.getApiTarget(isWatchRequest);
}
}
@@ -164,9 +164,9 @@ export class LensProxy {
}
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = this.clusterManager.getClusterForRequest(req)
const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler)
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
if (proxyTarget) {
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
res.setHeader("Access-Control-Allow-Origin", this.origin);
+5 -5
View File
@@ -1,13 +1,13 @@
import { app, remote } from "electron";
import winston from "winston"
import winston from "winston";
import { isDebugging } from "../common/vars";
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info"
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info";
const consoleOptions: winston.transports.ConsoleTransportOptions = {
handleExceptions: false,
level: logLevel,
}
};
const fileOptions: winston.transports.FileTransportOptions = {
handleExceptions: false,
@@ -17,7 +17,7 @@ const fileOptions: winston.transports.FileTransportOptions = {
maxsize: 16 * 1024,
maxFiles: 16,
tailable: true,
}
};
const logger = winston.createLogger({
format: winston.format.combine(
@@ -30,4 +30,4 @@ const logger = winston.createLogger({
],
});
export default logger
export default logger;
+23 -23
View File
@@ -1,4 +1,4 @@
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron"
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
import { autorun } from "mobx";
import { WindowManager } from "./window-manager";
import { appName, isMac, isWindows, isTestEnv } from "../common/vars";
@@ -25,14 +25,14 @@ export function showAbout(browserWindow: BrowserWindow) {
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Copyright 2020 Mirantis, Inc.`,
]
];
dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
buttons: ["Close"],
message: `Lens`,
detail: appInfo.join("\r\n")
})
});
}
export function buildMenu(windowManager: WindowManager) {
@@ -44,7 +44,7 @@ export function buildMenu(windowManager: WindowManager) {
function activeClusterOnly(menuItems: MenuItemConstructorOptions[]) {
if (!windowManager.activeClusterId) {
menuItems.forEach(item => {
item.enabled = false
item.enabled = false;
});
}
return menuItems;
@@ -61,7 +61,7 @@ export function buildMenu(windowManager: WindowManager) {
{
label: "About Lens",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow)
showAbout(browserWindow);
}
},
{ type: 'separator' },
@@ -69,14 +69,14 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click() {
navigate(preferencesURL())
navigate(preferencesURL());
}
},
{
label: 'Extensions',
accelerator: 'CmdOrCtrl+Shift+E',
click() {
navigate(extensionsURL())
navigate(extensionsURL());
}
},
{ type: 'separator' },
@@ -90,7 +90,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Quit',
accelerator: 'Cmd+Q',
click() {
exitApp()
exitApp();
}
}
]
@@ -103,7 +103,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Add Cluster',
accelerator: 'CmdOrCtrl+Shift+A',
click() {
navigate(addClusterURL())
navigate(addClusterURL());
}
},
...activeClusterOnly([
@@ -115,7 +115,7 @@ export function buildMenu(windowManager: WindowManager) {
params: {
clusterId: windowManager.activeClusterId
}
}))
}));
}
}
]),
@@ -125,14 +125,14 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Preferences',
accelerator: 'Ctrl+,',
click() {
navigate(preferencesURL())
navigate(preferencesURL());
}
},
{
label: 'Extensions',
accelerator: 'Ctrl+Shift+E',
click() {
navigate(extensionsURL())
navigate(extensionsURL());
}
}
]),
@@ -147,7 +147,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Exit',
accelerator: 'Alt+F4',
click() {
exitApp()
exitApp();
}
}
])
@@ -183,7 +183,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Forward',
accelerator: 'CmdOrCtrl+]',
click() {
webContents.getFocusedWebContents()?.goForward()
webContents.getFocusedWebContents()?.goForward();
}
},
{
@@ -209,7 +209,7 @@ export function buildMenu(windowManager: WindowManager) {
{
label: "What's new?",
click() {
navigate(whatsNewURL())
navigate(whatsNewURL());
},
},
{
@@ -222,7 +222,7 @@ export function buildMenu(windowManager: WindowManager) {
{
label: "About Lens",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow)
showAbout(browserWindow);
}
}
])
@@ -236,7 +236,7 @@ export function buildMenu(windowManager: WindowManager) {
edit: editMenu,
view: viewMenu,
help: helpMenu,
}
};
// Modify menu from extensions-api
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
@@ -244,12 +244,12 @@ export function buildMenu(windowManager: WindowManager) {
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
topMenu.push(menuItem);
} catch (err) {
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem })
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem });
}
})
});
if (!isMac) {
delete appMenu.mac
delete appMenu.mac;
}
const menu = Menu.buildFromTemplate(Object.values(appMenu));
@@ -259,9 +259,9 @@ export function buildMenu(windowManager: WindowManager) {
// this is a workaround for the test environment (spectron) not being able to directly access
// the application menus (https://github.com/electron-userland/spectron/issues/21)
ipcMain.on('test-menu-item-click', (event: IpcMainEvent, ...names: string[]) => {
let menu: Menu = Menu.getApplicationMenu()
let menu: Menu = Menu.getApplicationMenu();
const parentLabels: string[] = [];
let menuItem: MenuItem
let menuItem: MenuItem;
for (const name of names) {
parentLabels.push(name);
@@ -272,7 +272,7 @@ export function buildMenu(windowManager: WindowManager) {
menu = menuItem.submenu;
}
const menuPath: string = parentLabels.join(" -> ")
const menuPath: string = parentLabels.join(" -> ");
if (!menuItem) {
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
return;
+38 -38
View File
@@ -1,12 +1,12 @@
import * as WebSocket from "ws"
import * as pty from "node-pty"
import * as WebSocket from "ws";
import * as pty from "node-pty";
import { ShellSession } from "./shell-session";
import { v4 as uuid } from "uuid"
import * as k8s from "@kubernetes/client-node"
import { KubeConfig } from "@kubernetes/client-node"
import { Cluster } from "./cluster"
import { v4 as uuid } from "uuid";
import * as k8s from "@kubernetes/client-node";
import { KubeConfig } from "@kubernetes/client-node";
import { Cluster } from "./cluster";
import logger from "./logger";
import { appEventBus } from "../common/event-bus"
import { appEventBus } from "../common/event-bus";
export class NodeShellSession extends ShellSession {
protected nodeName: string;
@@ -14,23 +14,23 @@ export class NodeShellSession extends ShellSession {
protected kc: KubeConfig
constructor(socket: WebSocket, cluster: Cluster, nodeName: string) {
super(socket, cluster)
this.nodeName = nodeName
this.podId = `node-shell-${uuid()}`
this.kc = cluster.getProxyKubeconfig()
super(socket, cluster);
this.nodeName = nodeName;
this.podId = `node-shell-${uuid()}`;
this.kc = cluster.getProxyKubeconfig();
}
public async open() {
const shell = await this.kubectl.getPath()
let args = []
const shell = await this.kubectl.getPath();
let args = [];
if (this.createNodeShellPod(this.podId, this.nodeName)) {
await this.waitForRunningPod(this.podId).catch((error) => {
this.exit(1001)
})
this.exit(1001);
});
}
args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"]
args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"];
const shellEnv = await this.getCachedShellEnv()
const shellEnv = await this.getCachedShellEnv();
this.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || shellEnv["HOME"],
@@ -39,19 +39,19 @@ export class NodeShellSession extends ShellSession {
rows: 30,
});
this.running = true;
this.pipeStdout()
this.pipeStdin()
this.closeWebsocketOnProcessExit()
this.exitProcessOnWebsocketClose()
this.pipeStdout();
this.pipeStdin();
this.closeWebsocketOnProcessExit();
this.exitProcessOnWebsocketClose();
appEventBus.emit({name: "node-shell", action: "open"})
appEventBus.emit({name: "node-shell", action: "open"});
}
protected exit(code = 1000) {
if (this.podId) {
this.deleteNodeShellPod()
this.deleteNodeShellPod();
}
super.exit(code)
super.exit(code);
}
protected async createNodeShellPod(podId: string, nodeName: string) {
@@ -86,19 +86,19 @@ export class NodeShellSession extends ShellSession {
}
} as k8s.V1Pod;
await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => {
logger.error(error)
return false
})
return true
logger.error(error);
return false;
});
return true;
}
protected getKubeConfig() {
if (this.kc) {
return this.kc
return this.kc;
}
this.kc = new k8s.KubeConfig();
this.kc.loadFromFile(this.kubeconfigPath)
return this.kc
this.kc.loadFromFile(this.kubeconfigPath);
return this.kc;
}
protected waitForRunningPod(podId: string) {
@@ -110,36 +110,36 @@ export class NodeShellSession extends ShellSession {
// callback is called for each received object.
(_type, obj) => {
if (obj.metadata.name == podId && obj.status.phase === "Running") {
resolve(true)
resolve(true);
}
},
// done callback is called if the watch terminates normally
(err) => {
logger.error(err)
reject(false)
logger.error(err);
reject(false);
}
);
setTimeout(() => {
req.abort();
reject(false);
}, 120 * 1000);
})
});
}
protected deleteNodeShellPod() {
const kc = this.getKubeConfig();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
k8sApi.deleteNamespacedPod(this.podId, "kube-system")
k8sApi.deleteNamespacedPod(this.podId, "kube-system");
}
}
export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise<ShellSession> {
let shell: ShellSession;
if (nodeName) {
shell = new NodeShellSession(socket, cluster, nodeName)
shell = new NodeShellSession(socket, cluster, nodeName);
} else {
shell = new ShellSession(socket, cluster);
}
shell.open()
shell.open();
return shell;
}
+7 -7
View File
@@ -1,15 +1,15 @@
import net, { AddressInfo } from "net"
import logger from "./logger"
import net, { AddressInfo } from "net";
import logger from "./logger";
// todo: check https://github.com/http-party/node-portfinder ?
export async function getFreePort(): Promise<number> {
logger.debug("Lookup new free port..");
return new Promise((resolve, reject) => {
const server = net.createServer()
server.unref()
const server = net.createServer();
server.unref();
server.on("listening", () => {
const port = (server.address() as AddressInfo).port
const port = (server.address() as AddressInfo).port;
server.close(() => resolve(port));
logger.debug(`New port found: ${port}`);
});
@@ -17,6 +17,6 @@ export async function getFreePort(): Promise<number> {
logger.error(`Can't resolve new port: "${error}"`);
reject(error);
});
server.listen({ host: "127.0.0.1", port: 0 })
})
server.listen({ host: "127.0.0.1", port: 0 });
});
}
+10 -10
View File
@@ -1,5 +1,5 @@
import { EventEmitter } from 'events'
import { getFreePort } from "./port"
import { EventEmitter } from 'events';
import { getFreePort } from "./port";
let newPort = 0;
@@ -8,24 +8,24 @@ jest.mock("net", () => {
createServer() {
return new class MockServer extends EventEmitter {
listen = jest.fn(() => {
this.emit('listening')
return this
this.emit('listening');
return this;
})
address = () => {
newPort = Math.round(Math.random() * 10000)
newPort = Math.round(Math.random() * 10000);
return {
port: newPort
}
};
}
unref = jest.fn()
close = jest.fn(cb => cb())
}
};
},
}
};
});
describe("getFreePort", () => {
it("finds the next free port", async () => {
return expect(getFreePort()).resolves.toEqual(newPort);
})
})
});
});
+10 -10
View File
@@ -1,7 +1,7 @@
import { PrometheusLens } from "./lens"
import { CoreV1Api } from "@kubernetes/client-node"
import { PrometheusLens } from "./lens";
import { CoreV1Api } from "@kubernetes/client-node";
import { PrometheusService } from "./provider-registry";
import logger from "../logger"
import logger from "../logger";
export class PrometheusHelm extends PrometheusLens {
id = "helm"
@@ -9,21 +9,21 @@ export class PrometheusHelm extends PrometheusLens {
rateAccuracy = "5m"
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
const labelSelector = "app=prometheus,component=server,heritage=Helm"
const labelSelector = "app=prometheus,component=server,heritage=Helm";
try {
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector)
const service = serviceList.body.items[0]
if (!service) return
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector);
const service = serviceList.body.items[0];
if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
};
} catch(error) {
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`)
return
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
return;
}
}
}
+11 -11
View File
@@ -1,6 +1,6 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"
import logger from "../logger";
export class PrometheusLens implements PrometheusProvider {
id = "lens"
@@ -9,16 +9,16 @@ export class PrometheusLens implements PrometheusProvider {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
try {
const resp = await client.readNamespacedService("prometheus", "lens-metrics")
const service = resp.body
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
const service = resp.body;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
};
} catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
}
}
@@ -42,7 +42,7 @@ export class PrometheusLens implements PrometheusProvider {
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`,
fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`
}
};
case 'nodes':
return {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`,
@@ -51,7 +51,7 @@ export class PrometheusLens implements PrometheusProvider {
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)`
}
};
case 'pods':
return {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
@@ -63,21 +63,21 @@ export class PrometheusLens implements PrometheusProvider {
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
}
};
case 'pvc':
return {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
}
};
case 'ingress':
const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
}
};
}
}
}
+12 -12
View File
@@ -9,24 +9,24 @@ export class PrometheusOperator implements PrometheusProvider {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
try {
let service: V1Service
let service: V1Service;
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
if (!service) {
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector)
service = serviceList.body.items[0]
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
service = serviceList.body.items[0];
}
}
if (!service) return
if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
};
} catch(error) {
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`)
return
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
return;
}
}
@@ -50,7 +50,7 @@ export class PrometheusOperator implements PrometheusProvider {
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`
}
};
case 'nodes':
return {
memoryUsage: `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
@@ -59,7 +59,7 @@ export class PrometheusOperator implements PrometheusProvider {
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`
}
};
case 'pods':
return {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
@@ -71,12 +71,12 @@ export class PrometheusOperator implements PrometheusProvider {
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
}
};
case 'pvc':
return {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
}
};
case 'ingress':
const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
@@ -85,7 +85,7 @@ export class PrometheusOperator implements PrometheusProvider {
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
}
};
}
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
import { CoreV1Api } from "@kubernetes/client-node"
import { CoreV1Api } from "@kubernetes/client-node";
export type PrometheusClusterQuery = {
memoryUsage: string;
@@ -77,14 +77,14 @@ export class PrometheusProviderRegistry {
if (!this.prometheusProviders[type]) {
throw "Unknown Prometheus provider";
}
return this.prometheusProviders[type]
return this.prometheusProviders[type];
}
static registerProvider(key: string, provider: PrometheusProvider) {
this.prometheusProviders[key] = provider
this.prometheusProviders[key] = provider;
}
static getProviders(): PrometheusProvider[] {
return Object.values(this.prometheusProviders)
return Object.values(this.prometheusProviders);
}
}
+11 -11
View File
@@ -1,6 +1,6 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"
import logger from "../logger";
export class PrometheusStacklight implements PrometheusProvider {
id = "stacklight"
@@ -9,16 +9,16 @@ export class PrometheusStacklight implements PrometheusProvider {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
try {
const resp = await client.readNamespacedService("prometheus-server", "stacklight")
const service = resp.body
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
const service = resp.body;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
};
} catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
}
}
@@ -42,7 +42,7 @@ export class PrometheusStacklight implements PrometheusProvider {
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`,
fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`
}
};
case 'nodes':
return {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`,
@@ -51,7 +51,7 @@ export class PrometheusStacklight implements PrometheusProvider {
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`
}
};
case 'pods':
return {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
@@ -63,21 +63,21 @@ export class PrometheusStacklight implements PrometheusProvider {
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
}
};
case 'pvc':
return {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
}
};
case 'ingress':
const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
}
};
}
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
import * as util from "util"
import * as util from "util";
import { exec } from "child_process";
export const promiseExec = util.promisify(exec)
export const promiseExec = util.promisify(exec);
+7 -7
View File
@@ -1,18 +1,18 @@
import { app } from "electron"
import { app } from "electron";
const switchValue = app.commandLine.getSwitchValue("proxy-server")
const switchValue = app.commandLine.getSwitchValue("proxy-server");
export function mangleProxyEnv() {
let httpsProxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || ""
let httpsProxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || "";
delete process.env.HTTPS_PROXY
delete process.env.HTTP_PROXY
delete process.env.HTTPS_PROXY;
delete process.env.HTTP_PROXY;
if (switchValue !== "") {
httpsProxy = switchValue
httpsProxy = switchValue;
}
if (httpsProxy !== "") {
process.env.APP_HTTPS_PROXY = httpsProxy
process.env.APP_HTTPS_PROXY = httpsProxy;
}
}
+27 -27
View File
@@ -1,12 +1,12 @@
import type { Cluster } from "./cluster";
import { KubernetesObject } from "@kubernetes/client-node"
import { KubernetesObject } from "@kubernetes/client-node";
import { exec } from "child_process";
import fs from "fs";
import * as yaml from "js-yaml";
import path from "path";
import * as tempy from "tempy";
import logger from "./logger"
import { appEventBus } from "../common/event-bus"
import logger from "./logger";
import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils";
export class ResourceApplier {
@@ -15,58 +15,58 @@ export class ResourceApplier {
async apply(resource: KubernetesObject | any): Promise<string> {
resource = this.sanitizeObject(resource);
appEventBus.emit({name: "resource", action: "apply"})
appEventBus.emit({name: "resource", action: "apply"});
return await this.kubectlApply(yaml.safeDump(resource));
}
protected async kubectlApply(content: string): Promise<string> {
const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath()
const kubectlPath = await kubeCtl.getPath();
return new Promise<string>((resolve, reject) => {
const fileName = tempy.file({ name: "resource.yaml" })
fs.writeFileSync(fileName, content)
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`
const fileName = tempy.file({ name: "resource.yaml" });
fs.writeFileSync(fileName, content);
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`;
logger.debug("shooting manifests with: " + cmd);
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env)
const httpsProxy = this.cluster.preferences?.httpsProxy
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
const httpsProxy = this.cluster.preferences?.httpsProxy;
if (httpsProxy) {
execEnv["HTTPS_PROXY"] = httpsProxy
execEnv["HTTPS_PROXY"] = httpsProxy;
}
exec(cmd, { env: execEnv },
(error, stdout, stderr) => {
if (stderr != "") {
fs.unlinkSync(fileName)
reject(stderr)
return
fs.unlinkSync(fileName);
reject(stderr);
return;
}
fs.unlinkSync(fileName)
resolve(JSON.parse(stdout))
})
})
fs.unlinkSync(fileName);
resolve(JSON.parse(stdout));
});
});
}
public async kubectlApplyAll(resources: string[]): Promise<string> {
const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath()
const kubectlPath = await kubeCtl.getPath();
return new Promise((resolve, reject) => {
const tmpDir = tempy.directory()
const tmpDir = tempy.directory();
// Dump each resource into tmpDir
resources.forEach((resource, index) => {
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
})
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`
});
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`;
console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject("Error applying manifests:" + error);
}
if (stderr != "") {
reject(stderr)
return
reject(stderr);
return;
}
resolve(stdout)
})
})
resolve(stdout);
});
});
}
protected sanitizeObject(resource: KubernetesObject | any) {
+35 -35
View File
@@ -1,12 +1,12 @@
import Call from "@hapi/call"
import Subtext from "@hapi/subtext"
import http from "http"
import path from "path"
import { readFile } from "fs-extra"
import { Cluster } from "./cluster"
import Call from "@hapi/call";
import Subtext from "@hapi/subtext";
import http from "http";
import path from "path";
import { readFile } from "fs-extra";
import { Cluster } from "./cluster";
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
import logger from "./logger"
import logger from "./logger";
export interface RouterRequestOpts {
req: http.IncomingMessage;
@@ -43,25 +43,25 @@ export class Router {
public constructor() {
this.router = new Call.Router();
this.addRoutes()
this.addRoutes();
}
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
const url = new URL(req.url, "http://localhost");
const path = url.pathname
const method = req.method.toLowerCase()
const path = url.pathname;
const method = req.method.toLowerCase();
const matchingRoute = this.router.route(method, path);
const routeFound = !matchingRoute.isBoom;
if (routeFound) {
const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params });
await matchingRoute.route(request)
return true
await matchingRoute.route(request);
return true;
}
return false;
}
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest> {
const { req, res, url, cluster, params } = opts
const { req, res, url, cluster, params } = opts;
const { payload } = await Subtext.parse(req, null, {
parse: true,
output: "data",
@@ -76,7 +76,7 @@ export class Router {
query: url.searchParams,
payload: payload,
params: params
}
};
}
protected getMimeType(filename: string) {
@@ -92,7 +92,7 @@ export class Router {
woff2: "font/woff2",
ttf: "font/ttf"
};
return mimeTypes[path.extname(filename).slice(1)] || "text/plain"
return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
}
async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage, retryCount = 0) {
@@ -114,10 +114,10 @@ export class Router {
res.end();
} catch (err) {
if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString())
res.statusCode = 404
res.end()
return
logger.error("handleStaticFile:", err.toString());
res.statusCode = 404;
res.end();
return;
}
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1);
}
@@ -131,32 +131,32 @@ export class Router {
this.handleStaticFile(params.path, response, req);
});
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
// Watch API
this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute))
this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute));
// Metrics API
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute))
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute));
// Port-forward API
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute))
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute));
// Helm API
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmRoute.listCharts.bind(helmRoute))
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmRoute.getChart.bind(helmRoute))
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmRoute.getChartValues.bind(helmRoute))
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmRoute.listCharts.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmRoute.getChart.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmRoute.getChartValues.bind(helmRoute));
this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmRoute.installChart.bind(helmRoute))
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.updateRelease.bind(helmRoute))
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmRoute.rollbackRelease.bind(helmRoute))
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmRoute.listReleases.bind(helmRoute))
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.getRelease.bind(helmRoute))
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmRoute.getReleaseValues.bind(helmRoute))
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmRoute.getReleaseHistory.bind(helmRoute))
this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.deleteRelease.bind(helmRoute))
this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmRoute.installChart.bind(helmRoute));
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.updateRelease.bind(helmRoute));
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmRoute.rollbackRelease.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmRoute.listReleases.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.getRelease.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmRoute.getReleaseValues.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmRoute.getReleaseHistory.bind(helmRoute));
this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.deleteRelease.bind(helmRoute));
// Resource Applier API
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierRoute.applyResource.bind(resourceApplierRoute))
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierRoute.applyResource.bind(resourceApplierRoute));
}
}
+54 -54
View File
@@ -1,114 +1,114 @@
import { LensApiRequest } from "../router"
import { helmService } from "../helm/helm-service"
import { LensApi } from "../lens-api"
import logger from "../logger"
import { LensApiRequest } from "../router";
import { helmService } from "../helm/helm-service";
import { LensApi } from "../lens-api";
import logger from "../logger";
class HelmApiRoute extends LensApi {
public async listCharts(request: LensApiRequest) {
const { response } = request
const charts = await helmService.listCharts()
this.respondJson(response, charts)
const { response } = request;
const charts = await helmService.listCharts();
this.respondJson(response, charts);
}
public async getChart(request: LensApiRequest) {
const { params, query, response } = request
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"))
this.respondJson(response, chart)
const { params, query, response } = request;
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
this.respondJson(response, chart);
}
public async getChartValues(request: LensApiRequest) {
const { params, query, response } = request
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"))
this.respondJson(response, values)
const { params, query, response } = request;
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
this.respondJson(response, values);
}
public async installChart(request: LensApiRequest) {
const { payload, cluster, response } = request
const { payload, cluster, response } = request;
try {
const result = await helmService.installChart(cluster, payload)
this.respondJson(response, result, 201)
const result = await helmService.installChart(cluster, payload);
this.respondJson(response, result, 201);
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
logger.debug(error);
this.respondText(response, error, 422);
}
}
public async updateRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request
const { cluster, params, payload, response } = request;
try {
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload )
this.respondJson(response, result)
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
this.respondJson(response, result);
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
logger.debug(error);
this.respondText(response, error, 422);
}
}
public async rollbackRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request
const { cluster, params, payload, response } = request;
try {
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision)
this.respondJson(response, result)
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
this.respondJson(response, result);
} catch (error) {
logger.debug(error)
this.respondText(response, error)
logger.debug(error);
this.respondText(response, error);
}
}
public async listReleases(request: LensApiRequest) {
const { cluster, params, response } = request
const { cluster, params, response } = request;
try {
const result = await helmService.listReleases(cluster, params.namespace)
this.respondJson(response, result)
const result = await helmService.listReleases(cluster, params.namespace);
this.respondJson(response, result);
} catch(error) {
logger.debug(error)
this.respondText(response, error)
logger.debug(error);
this.respondText(response, error);
}
}
public async getRelease(request: LensApiRequest) {
const { cluster, params, response } = request
const { cluster, params, response } = request;
try {
const result = await helmService.getRelease(cluster, params.release, params.namespace)
this.respondJson(response, result)
const result = await helmService.getRelease(cluster, params.release, params.namespace);
this.respondJson(response, result);
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
logger.debug(error);
this.respondText(response, error, 422);
}
}
public async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response } = request
const { cluster, params, response } = request;
try {
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace)
this.respondText(response, result)
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
this.respondText(response, result);
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
logger.debug(error);
this.respondText(response, error, 422);
}
}
public async getReleaseHistory(request: LensApiRequest) {
const { cluster, params, response } = request
const { cluster, params, response } = request;
try {
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace)
this.respondJson(response, result)
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
this.respondJson(response, result);
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
logger.debug(error);
this.respondText(response, error, 422);
}
}
public async deleteRelease(request: LensApiRequest) {
const { cluster, params, response } = request
const { cluster, params, response } = request;
try {
const result = await helmService.deleteRelease(cluster, params.release, params.namespace)
this.respondJson(response, result)
const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
this.respondJson(response, result);
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
logger.debug(error);
this.respondText(response, error, 422);
}
}
}
export const helmRoute = new HelmApiRoute()
export const helmRoute = new HelmApiRoute();
+6 -6
View File
@@ -1,6 +1,6 @@
export * from "./kubeconfig-route"
export * from "./metrics-route"
export * from "./port-forward-route"
export * from "./watch-route"
export * from "./helm-route"
export * from "./resource-applier-route"
export * from "./kubeconfig-route";
export * from "./metrics-route";
export * from "./port-forward-route";
export * from "./watch-route";
export * from "./helm-route";
export * from "./resource-applier-route";
+10 -10
View File
@@ -1,10 +1,10 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Cluster } from "../cluster"
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { Cluster } from "../cluster";
import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
const tokenData = Buffer.from(secret.data["token"], "base64")
const tokenData = Buffer.from(secret.data["token"], "base64");
return {
'apiVersion': 'v1',
'kind': 'Config',
@@ -36,23 +36,23 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
}
],
'current-context': cluster.contextName
}
};
}
class KubeconfigRoute extends LensApi {
public async routeServiceAccountRoute(request: LensApiRequest) {
const { params, response, cluster} = request
const { params, response, cluster} = request;
const client = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace)
const secretList = await client.listNamespacedSecret(params.namespace);
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata;
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
});
const data = generateKubeConfig(params.account, secret, cluster);
this.respondJson(response, data)
this.respondJson(response, data);
}
}
export const kubeconfigRoute = new KubeconfigRoute()
export const kubeconfigRoute = new KubeconfigRoute();
+24 -24
View File
@@ -1,32 +1,32 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Cluster } from "../cluster"
import _ from "lodash"
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { Cluster } from "../cluster";
import _ from "lodash";
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;
}
// This is used for backoff retry tracking.
const MAX_ATTEMPTS = 5
const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), false)), true]
const MAX_ATTEMPTS = 5;
const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), false)), true];
// prometheus metrics loader
async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record<string, string>): Promise<any[]> {
const queries = promQueries.map(p => p.trim())
const loaders = new Map<string, Promise<any>>()
const queries = promQueries.map(p => p.trim());
const loaders = new Map<string, Promise<any>>();
async function loadMetric(query: string): Promise<any> {
async function loadMetricHelper(): Promise<any> {
for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry
try {
return await cluster.getMetrics(prometheusPath, { query, ...queryParams })
return await cluster.getMetrics(prometheusPath, { query, ...queryParams });
} catch (error) {
if (lastAttempt || error?.statusCode === 404) {
return {
status: error.toString(),
data: { result: [] },
}
};
}
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request
@@ -34,41 +34,41 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
}
}
return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query)
return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query);
}
return Promise.all(queries.map(loadMetric))
return Promise.all(queries.map(loadMetric));
}
class MetricsRoute extends LensApi {
async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
const queryParams: IMetricsQuery = Object.fromEntries(query.entries())
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
try {
const [prometheusPath, prometheusProvider] = await Promise.all([
cluster.contextHandler.getPrometheusPath(),
cluster.contextHandler.getPrometheusProvider()
])
]);
// return data in same structure as query
if (typeof payload === "string") {
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams)
this.respondJson(response, data)
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
this.respondJson(response, data);
} else if (Array.isArray(payload)) {
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams)
this.respondJson(response, data)
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
this.respondJson(response, data);
} else {
const queries = Object.entries(payload).map(([queryName, queryOpts]) => (
(prometheusProvider.getQueries(queryOpts) as Record<string, string>)[queryName]
))
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams)
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]))
this.respondJson(response, data)
));
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
this.respondJson(response, data);
}
} catch {
this.respondJson(response, {})
this.respondJson(response, {});
}
}
}
export const metricsRoute = new MetricsRoute()
export const metricsRoute = new MetricsRoute();
+35 -35
View File
@@ -1,11 +1,11 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { spawn, ChildProcessWithoutNullStreams } from "child_process"
import { Kubectl } from "../kubectl"
import { getFreePort } from "../port"
import { shell } from "electron"
import * as tcpPortUsed from "tcp-port-used"
import logger from "../logger"
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
import { Kubectl } from "../kubectl";
import { getFreePort } from "../port";
import { shell } from "electron";
import * as tcpPortUsed from "tcp-port-used";
import logger from "../logger";
class PortForward {
public static portForwards: PortForward[] = []
@@ -18,8 +18,8 @@ class PortForward {
pf.name == forward.name &&
pf.namespace == forward.namespace &&
pf.port == forward.port
)
})
);
});
}
public clusterId: string
@@ -32,56 +32,56 @@ class PortForward {
public localPort: number
constructor(obj: any) {
Object.assign(this, obj)
Object.assign(this, obj);
}
public async start() {
this.localPort = await getFreePort()
const kubectlBin = await Kubectl.bundled().getPath()
this.localPort = await getFreePort();
const kubectlBin = await Kubectl.bundled().getPath();
const args = [
"--kubeconfig", this.kubeConfig,
"port-forward",
"-n", this.namespace,
`${this.kind}/${this.name}`,
`${this.localPort}:${this.port}`
]
];
this.process = spawn(kubectlBin, args, {
env: process.env
})
PortForward.portForwards.push(this)
});
PortForward.portForwards.push(this);
this.process.on("exit", () => {
const index = PortForward.portForwards.indexOf(this)
const index = PortForward.portForwards.indexOf(this);
if (index > -1) {
PortForward.portForwards.splice(index, 1)
PortForward.portForwards.splice(index, 1);
}
})
});
try {
await tcpPortUsed.waitUntilUsed(this.localPort, 500, 15000)
return true
await tcpPortUsed.waitUntilUsed(this.localPort, 500, 15000);
return true;
} catch (error) {
this.process.kill()
return false
this.process.kill();
return false;
}
}
public open() {
shell.openExternal(`http://localhost:${this.localPort}`)
shell.openExternal(`http://localhost:${this.localPort}`);
}
}
class PortForwardRoute extends LensApi {
public async routePortForward(request: LensApiRequest) {
const { params, response, cluster} = request
const { namespace, port, resourceType, resourceName } = params
const { params, response, cluster} = request;
const { namespace, port, resourceType, resourceName } = params;
let portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName,
namespace: namespace, port: port
})
});
if (!portForward) {
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`)
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
portForward = new PortForward({
clusterId: cluster.id,
kind: resourceType,
@@ -89,20 +89,20 @@ class PortForwardRoute extends LensApi {
name: resourceName,
port: port,
kubeConfig: cluster.getProxyKubeconfigPath()
})
const started = await portForward.start()
});
const started = await portForward.start();
if (!started) {
this.respondJson(response, {
message: "Failed to open port-forward"
}, 400)
return
}, 400);
return;
}
}
portForward.open()
portForward.open();
this.respondJson(response, {})
this.respondJson(response, {});
}
}
export const portForwardRoute = new PortForwardRoute()
export const portForwardRoute = new PortForwardRoute();
+7 -7
View File
@@ -1,17 +1,17 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { ResourceApplier } from "../resource-applier"
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { ResourceApplier } from "../resource-applier";
class ResourceApplierApiRoute extends LensApi {
public async applyResource(request: LensApiRequest) {
const { response, cluster, payload } = request
const { response, cluster, payload } = request;
try {
const resource = await new ResourceApplier(cluster).apply(payload);
this.respondJson(response, [resource], 200)
this.respondJson(response, [resource], 200);
} catch (error) {
this.respondText(response, error, 422)
this.respondText(response, error, 422);
}
}
}
export const resourceApplierRoute = new ResourceApplierApiRoute()
export const resourceApplierRoute = new ResourceApplierApiRoute();
+45 -45
View File
@@ -1,9 +1,9 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Watch, KubeConfig } from "@kubernetes/client-node"
import { ServerResponse } from "http"
import { Request } from "request"
import logger from "../logger"
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { Watch, KubeConfig } from "@kubernetes/client-node";
import { ServerResponse } from "http";
import { Request } from "request";
import logger from "../logger";
class ApiWatcher {
private apiUrl: string
@@ -14,40 +14,40 @@ class ApiWatcher {
private eventBuffer: any[] = []
constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
this.apiUrl = apiUrl
this.watch = new Watch(kubeConfig)
this.response = response
this.apiUrl = apiUrl;
this.watch = new Watch(kubeConfig);
this.response = response;
}
public async start() {
if (this.processor) {
clearInterval(this.processor)
clearInterval(this.processor);
}
this.processor = setInterval(() => {
const events = this.eventBuffer.splice(0)
events.map(event => this.sendEvent(event))
this.response.flushHeaders()
}, 50)
this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this))
const events = this.eventBuffer.splice(0);
events.map(event => this.sendEvent(event));
this.response.flushHeaders();
}, 50);
this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this));
}
public stop() {
if (!this.watchRequest) { return }
if (!this.watchRequest) { return; }
if (this.processor) {
clearInterval(this.processor)
clearInterval(this.processor);
}
logger.debug("Stopping watcher for api: " + this.apiUrl)
logger.debug("Stopping watcher for api: " + this.apiUrl);
try {
this.watchRequest.abort()
this.watchRequest.abort();
this.sendEvent({
type: "STREAM_END",
url: this.apiUrl,
status: 410,
})
logger.debug("watch aborted")
});
logger.debug("watch aborted");
} catch (error) {
logger.error("Watch abort errored:" + error)
logger.error("Watch abort errored:" + error);
}
}
@@ -55,12 +55,12 @@ class ApiWatcher {
this.eventBuffer.push({
type: phase,
object: obj
})
});
}
private doneHandler(error: Error) {
if (error) logger.warn("watch ended: " + error.toString())
this.watchRequest.abort()
if (error) logger.warn("watch ended: " + error.toString());
this.watchRequest.abort();
}
private sendEvent(evt: any) {
@@ -72,40 +72,40 @@ class ApiWatcher {
class WatchRoute extends LensApi {
public async routeWatch(request: LensApiRequest) {
const { params, response, cluster} = request
const apis: string[] = request.query.getAll("api")
const watchers: ApiWatcher[] = []
const { params, response, cluster} = request;
const apis: string[] = request.query.getAll("api");
const watchers: ApiWatcher[] = [];
if (!apis.length) {
this.respondJson(response, {
message: "Empty request. Query params 'api' are not provided.",
example: "?api=/api/v1/pods&api=/api/v1/nodes",
}, 400)
return
}, 400);
return;
}
response.setHeader("Content-Type", "text/event-stream")
response.setHeader("Cache-Control", "no-cache")
response.setHeader("Connection", "keep-alive")
logger.debug("watch using kubeconfig:" + JSON.stringify(cluster.getProxyKubeconfig(), null, 2))
response.setHeader("Content-Type", "text/event-stream");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
logger.debug("watch using kubeconfig:" + JSON.stringify(cluster.getProxyKubeconfig(), null, 2));
apis.forEach(apiUrl => {
const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response)
watcher.start()
watchers.push(watcher)
})
const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
watcher.start();
watchers.push(watcher);
});
request.raw.req.on("close", () => {
logger.debug("Watch request closed")
watchers.map(watcher => watcher.stop())
})
logger.debug("Watch request closed");
watchers.map(watcher => watcher.stop());
});
request.raw.req.on("end", () => {
logger.debug("Watch request ended")
watchers.map(watcher => watcher.stop())
})
logger.debug("Watch request ended");
watchers.map(watcher => watcher.stop());
});
}
}
export const watchRoute = new WatchRoute()
export const watchRoute = new WatchRoute();
+81 -81
View File
@@ -1,15 +1,15 @@
import * as pty from "node-pty"
import * as WebSocket from "ws"
import * as pty from "node-pty";
import * as WebSocket from "ws";
import { EventEmitter } from "events";
import path from "path"
import shellEnv from "shell-env"
import { app } from "electron"
import { Kubectl } from "./kubectl"
import { Cluster } from "./cluster"
import path from "path";
import shellEnv from "shell-env";
import { app } from "electron";
import { Kubectl } from "./kubectl";
import { Cluster } from "./cluster";
import { ClusterPreferences } from "../common/cluster-store";
import { helmCli } from "./helm/helm-cli"
import { helmCli } from "./helm/helm-cli";
import { isWindows } from "../common/vars";
import { appEventBus } from "../common/event-bus"
import { appEventBus } from "../common/event-bus";
import { userStore } from "../common/user-store";
export class ShellSession extends EventEmitter {
@@ -28,22 +28,22 @@ export class ShellSession extends EventEmitter {
protected clusterId: string;
constructor(socket: WebSocket, cluster: Cluster) {
super()
this.websocket = socket
this.kubeconfigPath = cluster.getProxyKubeconfigPath()
this.kubectl = new Kubectl(cluster.version)
this.preferences = cluster.preferences || {}
this.clusterId = cluster.id
super();
this.websocket = socket;
this.kubeconfigPath = cluster.getProxyKubeconfigPath();
this.kubectl = new Kubectl(cluster.version);
this.preferences = cluster.preferences || {};
this.clusterId = cluster.id;
}
public async open() {
this.kubectlBinDir = await this.kubectl.binDir()
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath()
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences)
this.helmBinDir = helmCli.getBinaryDir()
const env = await this.getCachedShellEnv()
const shell = env.PTYSHELL
const args = await this.getShellArgs(shell)
this.kubectlBinDir = await this.kubectl.binDir();
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath();
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences);
this.helmBinDir = helmCli.getBinaryDir();
const env = await this.getCachedShellEnv();
const shell = env.PTYSHELL;
const args = await this.getShellArgs(shell);
this.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || env.HOME,
@@ -53,138 +53,138 @@ export class ShellSession extends EventEmitter {
});
this.running = true;
this.pipeStdout()
this.pipeStdin()
this.closeWebsocketOnProcessExit()
this.exitProcessOnWebsocketClose()
this.pipeStdout();
this.pipeStdin();
this.closeWebsocketOnProcessExit();
this.exitProcessOnWebsocketClose();
appEventBus.emit({name: "shell", action: "open"})
appEventBus.emit({name: "shell", action: "open"});
}
protected cwd(): string {
if(!this.preferences || !this.preferences.terminalCWD || this.preferences.terminalCWD === "") {
return null
return null;
}
return this.preferences.terminalCWD
return this.preferences.terminalCWD;
}
protected async getShellArgs(shell: string): Promise<Array<string>> {
switch(path.basename(shell)) {
case "powershell.exe":
return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.helmBinDir};${this.kubectlPathDir};$Env:PATH"}`]
return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.helmBinDir};${this.kubectlPathDir};$Env:PATH"}`];
case "bash":
return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')]
return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')];
case "fish":
return ["--login", "--init-command", `export PATH="${this.helmBinDir}:${this.kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`]
return ["--login", "--init-command", `export PATH="${this.helmBinDir}:${this.kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`];
case "zsh":
return ["--login"]
return ["--login"];
default:
return []
return [];
}
}
protected async getCachedShellEnv() {
let env = ShellSession.shellEnvs.get(this.clusterId)
let env = ShellSession.shellEnvs.get(this.clusterId);
if (!env) {
env = await this.getShellEnv()
ShellSession.shellEnvs.set(this.clusterId, env)
env = await this.getShellEnv();
ShellSession.shellEnvs.set(this.clusterId, env);
} else {
// refresh env in the background
this.getShellEnv().then((shellEnv: any) => {
ShellSession.shellEnvs.set(this.clusterId, shellEnv)
})
ShellSession.shellEnvs.set(this.clusterId, shellEnv);
});
}
return env
return env;
}
protected async getShellEnv() {
const env = JSON.parse(JSON.stringify(await shellEnv()))
const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter)
const env = JSON.parse(JSON.stringify(await shellEnv()));
const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter);
if(isWindows) {
env["SystemRoot"] = process.env.SystemRoot
env["PTYSHELL"] = "powershell.exe"
env["PATH"] = pathStr
env["SystemRoot"] = process.env.SystemRoot;
env["PTYSHELL"] = "powershell.exe";
env["PATH"] = pathStr;
} else if(typeof(process.env.SHELL) != "undefined") {
env["PTYSHELL"] = process.env.SHELL
env["PATH"] = pathStr
env["PTYSHELL"] = process.env.SHELL;
env["PATH"] = pathStr;
} else {
env["PTYSHELL"] = "" // blank runs the system default shell
env["PTYSHELL"] = ""; // blank runs the system default shell
}
if(path.basename(env["PTYSHELL"]) === "zsh") {
env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME
env["ZDOTDIR"] = this.kubectlBinDir
env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME;
env["ZDOTDIR"] = this.kubectlBinDir;
}
env["PTYPID"] = process.pid.toString()
env["KUBECONFIG"] = this.kubeconfigPath
env["TERM_PROGRAM"] = app.getName()
env["TERM_PROGRAM_VERSION"] = app.getVersion()
env["PTYPID"] = process.pid.toString();
env["KUBECONFIG"] = this.kubeconfigPath;
env["TERM_PROGRAM"] = app.getName();
env["TERM_PROGRAM_VERSION"] = app.getVersion();
if (this.preferences.httpsProxy) {
env["HTTPS_PROXY"] = this.preferences.httpsProxy
env["HTTPS_PROXY"] = this.preferences.httpsProxy;
}
const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]]
env["NO_PROXY"] = no_proxy.filter(address => !!address).join()
const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]];
env["NO_PROXY"] = no_proxy.filter(address => !!address).join();
if (env.DEBUG) { // do not pass debug option to bash
delete env["DEBUG"]
delete env["DEBUG"];
}
return(env)
return(env);
}
protected pipeStdout() {
// send shell output to websocket
this.shellProcess.onData(((data: string) => {
this.sendResponse(data)
this.sendResponse(data);
}));
}
protected pipeStdin() {
// write websocket messages to shellProcess
this.websocket.on("message", (data: string) => {
if (!this.running) { return }
if (!this.running) { return; }
const message = Buffer.from(data.slice(1, data.length), "base64").toString()
const message = Buffer.from(data.slice(1, data.length), "base64").toString();
switch (data[0]) {
case "0":
this.shellProcess.write(message)
this.shellProcess.write(message);
break;
case "4":
const resizeMsgObj = JSON.parse(message)
this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"])
const resizeMsgObj = JSON.parse(message);
this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"]);
break;
case "9":
this.emit('newToken', message)
this.emit('newToken', message);
break;
}
})
});
}
protected exit(code = 1000) {
if (this.websocket.readyState == this.websocket.OPEN) this.websocket.close(code)
this.emit('exit')
if (this.websocket.readyState == this.websocket.OPEN) this.websocket.close(code);
this.emit('exit');
}
protected closeWebsocketOnProcessExit() {
this.shellProcess.onExit(({ exitCode }) => {
this.running = false
let timeout = 0
this.running = false;
let timeout = 0;
if (exitCode > 0) {
this.sendResponse("Terminal will auto-close in 15 seconds ...")
timeout = 15*1000
this.sendResponse("Terminal will auto-close in 15 seconds ...");
timeout = 15*1000;
}
setTimeout(() => {
this.exit()
}, timeout)
this.exit();
}, timeout);
});
}
protected exitProcessOnWebsocketClose() {
this.websocket.on("close", () => {
this.killShellProcess()
})
this.killShellProcess();
});
}
protected killShellProcess(){
@@ -192,17 +192,17 @@ export class ShellSession extends EventEmitter {
// 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 (isWindows) {
try {
process.kill(this.shellProcess.pid)
process.kill(this.shellProcess.pid);
} catch(e) {
return
return;
}
} else {
this.shellProcess.kill()
this.shellProcess.kill();
}
}
}
protected sendResponse(msg: string) {
this.websocket.send("1" + Buffer.from(msg).toString("base64"))
this.websocket.send("1" + Buffer.from(msg).toString("base64"));
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
import shellEnv from "shell-env"
import shellEnv from "shell-env";
import os from "os";
import { app } from "electron";
import logger from "./logger";
@@ -19,7 +19,7 @@ export async function shellSync() {
try {
envVars = await shellEnv(shell);
} catch (error) {
logger.error(`shellEnv: ${error}`)
logger.error(`shellEnv: ${error}`);
}
const env: Env = JSON.parse(JSON.stringify(envVars));
@@ -27,12 +27,12 @@ export async function shellSync() {
// the LANG env var expects an underscore instead of electron's dash
env.LANG = `${app.getLocale().replace('-', '_')}.UTF-8`;
} else if (!env.LANG.endsWith(".UTF-8")) {
env.LANG += ".UTF-8"
env.LANG += ".UTF-8";
}
// Overwrite PATH on darwin
if (process.env.NODE_ENV === "production" && process.platform === "darwin") {
process.env["PATH"] = env.PATH
process.env["PATH"] = env.PATH;
}
// The spread operator allows joining of objects. The precedence is last to first.
+13 -13
View File
@@ -1,6 +1,6 @@
import path from "path"
import packageInfo from "../../package.json"
import { dialog, Menu, NativeImage, nativeTheme, Tray } from "electron"
import path from "path";
import packageInfo from "../../package.json";
import { dialog, Menu, NativeImage, nativeTheme, Tray } from "electron";
import { autorun } from "mobx";
import { showAbout } from "./menu";
import { AppUpdater } from "./app-updater";
@@ -24,7 +24,7 @@ export function getTrayIcon(isDark = nativeTheme.shouldUseDarkColors): string {
__static,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
`tray_icon${isDark ? "_dark" : ""}.png`
)
);
}
export function initTray(windowManager: WindowManager) {
@@ -35,18 +35,18 @@ export function initTray(windowManager: WindowManager) {
} catch (err) {
logger.error(`[TRAY]: building failed: ${err}`);
}
})
});
return () => {
dispose();
tray?.destroy();
tray = null;
}
};
}
export function buildTray(icon: string | NativeImage, menu: Menu) {
if (!tray) {
tray = new Tray(icon)
tray.setToolTip(packageInfo.description)
tray = new Tray(icon);
tray.setToolTip(packageInfo.description);
tray.setIgnoreDoubleClickEvents(true);
}
@@ -70,7 +70,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
{
label: "Open Lens",
async click() {
await windowManager.ensureMainWindow()
await windowManager.ensureMainWindow();
},
},
{
@@ -98,9 +98,9 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
clusterStore.setActive(clusterId);
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
}
}
};
})
}
};
}),
},
{
@@ -112,7 +112,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
dialog.showMessageBoxSync(browserWindow, {
message: "No updates available",
type: "info",
})
});
}
},
},
@@ -120,7 +120,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
{
label: 'Quit App',
click() {
exitApp()
exitApp();
}
}
]);
+18 -18
View File
@@ -1,9 +1,9 @@
import type { ClusterId } from "../common/cluster-store";
import { observable } from "mobx";
import { app, BrowserWindow, dialog, shell, webContents } from "electron"
import windowStateKeeper from "electron-window-state"
import { appEventBus } from "../common/event-bus"
import { subscribeToBroadcast } from "../common/ipc"
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus";
import { subscribeToBroadcast } from "../common/ipc";
import { initMenu } from "./menu";
import { initTray } from "./tray";
import { Singleton } from "../common/utils";
@@ -26,7 +26,7 @@ export class WindowManager extends Singleton {
}
get mainUrl() {
return `http://localhost:${this.proxyPort}`
return `http://localhost:${this.proxyPort}`;
}
async initMainWindow(showSplash = true) {
@@ -63,14 +63,14 @@ export class WindowManager extends Singleton {
shell.openExternal(url);
});
this.mainWindow.webContents.on("dom-ready", () => {
appEventBus.emit({name: "app", action: "dom-ready"})
})
appEventBus.emit({name: "app", action: "dom-ready"});
});
this.mainWindow.on("focus", () => {
appEventBus.emit({name: "app", action: "focus"})
})
appEventBus.emit({name: "app", action: "focus"});
});
this.mainWindow.on("blur", () => {
appEventBus.emit({name: "app", action: "blur"})
})
appEventBus.emit({name: "app", action: "blur"});
});
// clean up
this.mainWindow.on("closed", () => {
@@ -78,16 +78,16 @@ export class WindowManager extends Singleton {
this.mainWindow = null;
this.splashWindow = null;
app.dock?.hide(); // hide icon in dock (mac-os)
})
});
}
try {
if (showSplash) await this.showSplash();
await this.mainWindow.loadURL(this.mainUrl);
this.mainWindow.show();
this.splashWindow?.close();
appEventBus.emit({ name: "app", action: "start" })
appEventBus.emit({ name: "app", action: "start" });
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString())
dialog.showErrorBox("ERROR!", err.toString());
}
}
@@ -103,7 +103,7 @@ export class WindowManager extends Singleton {
// track visible cluster from ui
subscribeToBroadcast("cluster-view:current-id", (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
})
});
}
async ensureMainWindow(): Promise<BrowserWindow> {
@@ -126,11 +126,11 @@ export class WindowManager extends Singleton {
channel: "renderer:navigate",
frameId: frameId,
data: [url],
})
});
}
reload() {
const frameId = clusterFrameMap.get(this.activeClusterId)
const frameId = clusterFrameMap.get(this.activeClusterId);
if (frameId) {
this.sendToView({ channel: "renderer:reload", frameId });
} else {
@@ -169,7 +169,7 @@ export class WindowManager extends Singleton {
this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => {
dispose();
delete this.disposers[name]
delete this.disposers[name];
});
}
}