mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Enforce semicolons in eslint
Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
1 parent
0f3f5611ba
commit
1477bb8274
531 files changed
+3979
-3975
No files matched your search
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 || "";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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>();
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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";
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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];
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user