mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Cascade different refactorings to eliminate global shared state from app paths
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
1 parent
908a40975e
commit
9d33fff906
336 files changed
+9105
-3307
No files matched your search
@@ -45,25 +45,29 @@ jest.mock("winston", () => ({
|
||||
}));
|
||||
|
||||
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 { Cluster } from "../cluster";
|
||||
import { Kubectl } from "../kubectl";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { Kubectl } from "../kubectl/kubectl";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { ClusterModel } from "../../common/cluster-types";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
|
||||
describe("create clusters", () => {
|
||||
beforeEach(() => {
|
||||
let cluster: Cluster;
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
let c: Cluster;
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
const mockOpts = {
|
||||
"minikube-config.yml": JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
@@ -89,8 +93,14 @@ describe("create clusters", () => {
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
|
||||
c = new Cluster({
|
||||
|
||||
cluster = createCluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
@@ -102,48 +112,39 @@ describe("create clusters", () => {
|
||||
});
|
||||
|
||||
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(cluster.apiUrl).toBe("https://192.168.64.3:8443");
|
||||
});
|
||||
|
||||
it("reconnect should not throw if contextHandler is missing", () => {
|
||||
expect(() => c.reconnect()).not.toThrowError();
|
||||
expect(() => cluster.reconnect()).not.toThrowError();
|
||||
});
|
||||
|
||||
it("disconnect should not throw if contextHandler is missing", () => {
|
||||
expect(() => c.disconnect()).not.toThrowError();
|
||||
expect(() => cluster.disconnect()).not.toThrowError();
|
||||
});
|
||||
|
||||
it("activating cluster should try to connect to cluster and do a refresh", async () => {
|
||||
|
||||
const c = new class extends Cluster {
|
||||
// only way to mock protected methods, without these we leak promises
|
||||
protected bindEvents() {
|
||||
return;
|
||||
}
|
||||
async ensureKubectl() {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}({
|
||||
const cluster = createCluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
|
||||
c.contextHandler = {
|
||||
cluster.contextHandler = {
|
||||
ensureServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
} as any;
|
||||
|
||||
jest.spyOn(c, "reconnect");
|
||||
jest.spyOn(c, "canI");
|
||||
jest.spyOn(c, "refreshConnectionStatus");
|
||||
jest.spyOn(cluster, "reconnect");
|
||||
jest.spyOn(cluster, "canI");
|
||||
jest.spyOn(cluster, "refreshConnectionStatus");
|
||||
|
||||
await c.activate();
|
||||
await cluster.activate();
|
||||
|
||||
expect(c.reconnect).toBeCalled();
|
||||
expect(c.refreshConnectionStatus).toBeCalled();
|
||||
expect(cluster.reconnect).toBeCalled();
|
||||
expect(cluster.refreshConnectionStatus).toBeCalled();
|
||||
|
||||
c.disconnect();
|
||||
cluster.disconnect();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
});
|
||||
@@ -20,10 +20,12 @@
|
||||
*/
|
||||
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { ContextHandler } from "../context-handler";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
|
||||
import mockFs from "mock-fs";
|
||||
import { AppPaths } from "../../common/app-paths";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@@ -77,25 +79,28 @@ class TestProvider extends PrometheusProvider {
|
||||
}
|
||||
}
|
||||
|
||||
function getHandler() {
|
||||
return new ContextHandler(({
|
||||
getProxyKubeconfig: (): any => ({
|
||||
makeApiClient: (): any => undefined,
|
||||
}),
|
||||
apiUrl: "http://localhost:81",
|
||||
}) as any);
|
||||
}
|
||||
|
||||
AppPaths.init();
|
||||
const clusterStub = {
|
||||
getProxyKubeconfig: (): any => ({
|
||||
makeApiClient: (): any => undefined,
|
||||
}),
|
||||
apiUrl: "http://localhost:81",
|
||||
} as Cluster;
|
||||
|
||||
describe("ContextHandler", () => {
|
||||
beforeEach(() => {
|
||||
let createContextHandler: (cluster: Cluster) => ContextHandler;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs({
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
createContextHandler = di.inject(createContextHandlerInjectable);
|
||||
|
||||
PrometheusProviderRegistry.createInstance();
|
||||
UserStore.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -124,7 +129,12 @@ describe("ContextHandler", () => {
|
||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||
}
|
||||
|
||||
expect(() => (getHandler() as any).getPrometheusService()).rejects.toBeDefined();
|
||||
expect(() => {
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
|
||||
return contextHandler.getPrometheusService();
|
||||
}).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -150,7 +160,10 @@ describe("ContextHandler", () => {
|
||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||
}
|
||||
|
||||
const service = await (getHandler() as any).getPrometheusService();
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
expect(service.id === `id_${failures}`);
|
||||
});
|
||||
@@ -178,7 +191,10 @@ describe("ContextHandler", () => {
|
||||
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
|
||||
}
|
||||
|
||||
const service = await (getHandler() as any).getPrometheusService();
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
expect(service.id === "id_0");
|
||||
});
|
||||
@@ -211,8 +227,11 @@ describe("ContextHandler", () => {
|
||||
for (let i = 0; i < afterSuccesses; i += 1) {
|
||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||
}
|
||||
|
||||
const service = await (getHandler() as any).getPrometheusService();
|
||||
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
expect(service.id === "id_0");
|
||||
});
|
||||
@@ -224,8 +243,11 @@ describe("ContextHandler", () => {
|
||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Failure));
|
||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
|
||||
const service = await (getHandler() as any).getPrometheusService();
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
expect(service.id).not.toBe("id_2");
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { ClusterModel } from "../../common/cluster-types";
|
||||
|
||||
jest.mock("winston", () => ({
|
||||
format: {
|
||||
colorize: jest.fn(),
|
||||
@@ -48,11 +50,11 @@ jest.mock("../../common/ipc");
|
||||
jest.mock("child_process");
|
||||
jest.mock("tcp-port-used");
|
||||
|
||||
import { Cluster } from "../cluster";
|
||||
import { KubeAuthProxy } from "../kube-auth-proxy";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
|
||||
import { broadcastMessage } from "../../common/ipc";
|
||||
import { ChildProcess, spawn } from "child_process";
|
||||
import { bundledKubectlPath, Kubectl } from "../kubectl";
|
||||
import { bundledKubectlPath, Kubectl } from "../kubectl/kubectl";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { waitUntilUsed } from "tcp-port-used";
|
||||
import { EventEmitter, Readable } from "stream";
|
||||
@@ -60,7 +62,9 @@ import { UserStore } from "../../common/user-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import mockFs from "mock-fs";
|
||||
import { AppPaths } from "../../common/app-paths";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
@@ -68,25 +72,11 @@ const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcas
|
||||
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
||||
const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>;
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getName: () => "lens",
|
||||
setName: jest.fn(),
|
||||
setPath: jest.fn(),
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
AppPaths.init();
|
||||
|
||||
describe("kube auth proxy tests", () => {
|
||||
beforeEach(() => {
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const mockMinikubeConfig = {
|
||||
@@ -115,7 +105,16 @@ describe("kube auth proxy tests", () => {
|
||||
"tmp": {},
|
||||
};
|
||||
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs(mockMinikubeConfig);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
|
||||
|
||||
UserStore.createInstance();
|
||||
});
|
||||
|
||||
@@ -125,7 +124,13 @@ describe("kube auth proxy tests", () => {
|
||||
});
|
||||
|
||||
it("calling exit multiple times shouldn't throw", async () => {
|
||||
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube" }), {});
|
||||
const cluster = createCluster({
|
||||
id: "foobar",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
contextName: "minikube",
|
||||
});
|
||||
|
||||
const kap = createKubeAuthProxy(cluster, {});
|
||||
|
||||
kap.exit();
|
||||
kap.exit();
|
||||
@@ -211,9 +216,13 @@ describe("kube auth proxy tests", () => {
|
||||
});
|
||||
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
|
||||
|
||||
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube" });
|
||||
const cluster = createCluster({
|
||||
id: "foobar",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
contextName: "minikube",
|
||||
});
|
||||
|
||||
proxy = new KubeAuthProxy(cluster, {});
|
||||
proxy = createKubeAuthProxy(cluster, {});
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast errors", async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
|
||||
const logger = {
|
||||
silly: jest.fn(),
|
||||
@@ -46,41 +47,28 @@ jest.mock("winston", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import { KubeconfigManager } from "../kubeconfig-manager";
|
||||
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||
import mockFs from "mock-fs";
|
||||
import { Cluster } from "../cluster";
|
||||
import type { ContextHandler } from "../context-handler";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import fse from "fs-extra";
|
||||
import { loadYaml } from "@kubernetes/client-node";
|
||||
import { Console } from "console";
|
||||
import * as path from "path";
|
||||
import { AppPaths } from "../../common/app-paths";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getName: () => "lens",
|
||||
setName: jest.fn(),
|
||||
setPath: jest.fn(),
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
|
||||
describe("kubeconfig manager tests", () => {
|
||||
let cluster: Cluster;
|
||||
let contextHandler: ContextHandler;
|
||||
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
|
||||
|
||||
beforeEach(() => {
|
||||
const mockOpts = {
|
||||
"minikube-config.yml": JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
@@ -107,14 +95,22 @@ describe("kubeconfig manager tests", () => {
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
cluster = new Cluster({
|
||||
await di.runSetups();
|
||||
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
||||
|
||||
cluster = createCluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
contextHandler = {
|
||||
|
||||
cluster.contextHandler = {
|
||||
ensureServer: () => Promise.resolve(),
|
||||
} as any;
|
||||
|
||||
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
|
||||
});
|
||||
|
||||
@@ -123,10 +119,10 @@ describe("kubeconfig manager tests", () => {
|
||||
});
|
||||
|
||||
it("should create 'temp' kube config with proxy", async () => {
|
||||
const kubeConfManager = new KubeconfigManager(cluster, contextHandler);
|
||||
const kubeConfManager = createKubeconfigManager(cluster);
|
||||
|
||||
expect(logger.error).not.toBeCalled();
|
||||
expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`);
|
||||
expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`);
|
||||
// this causes an intermittent "ENXIO: no such device or address, read" error
|
||||
// const file = await fse.readFile(await kubeConfManager.getPath());
|
||||
const file = fse.readFileSync(await kubeConfManager.getPath());
|
||||
@@ -138,7 +134,8 @@ describe("kubeconfig manager tests", () => {
|
||||
});
|
||||
|
||||
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
|
||||
const kubeConfManager = new KubeconfigManager(cluster, contextHandler);
|
||||
const kubeConfManager = createKubeconfigManager(cluster);
|
||||
|
||||
const configPath = await kubeConfManager.getPath();
|
||||
|
||||
expect(await fse.pathExists(configPath)).toBe(true);
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { AppPaths } from "../../common/app-paths";
|
||||
import { Router } from "../router";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
@@ -38,8 +37,6 @@ jest.mock("electron", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
describe("Router", () => {
|
||||
it("blocks path traversal attacks", async () => {
|
||||
const response: any = {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable";
|
||||
|
||||
const appNameInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(electronAppInjectable).name,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default appNameInjectable;
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import {
|
||||
DependencyInjectionContainer,
|
||||
getInjectable,
|
||||
lifecycleEnum,
|
||||
} from "@ogre-tools/injectable";
|
||||
|
||||
import {
|
||||
appPathsInjectionToken,
|
||||
appPathsIpcChannel,
|
||||
} from "../../common/app-paths/app-path-injection-token";
|
||||
|
||||
import registerChannelInjectable from "./register-channel/register-channel.injectable";
|
||||
import { getAppPaths } from "./get-app-paths";
|
||||
import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable";
|
||||
import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable";
|
||||
import path from "path";
|
||||
import appNameInjectable from "./app-name/app-name.injectable";
|
||||
import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable";
|
||||
|
||||
const appPathsInjectable = getInjectable({
|
||||
setup: (di) => {
|
||||
const directoryForIntegrationTesting = di.inject(
|
||||
directoryForIntegrationTestingInjectable,
|
||||
);
|
||||
|
||||
if (directoryForIntegrationTesting) {
|
||||
setupPathForAppDataInIntegrationTesting(di, directoryForIntegrationTesting);
|
||||
}
|
||||
|
||||
setupPathForUserData(di);
|
||||
registerAppPathsChannel(di);
|
||||
},
|
||||
|
||||
instantiate: (di) =>
|
||||
getAppPaths({ getAppPath: di.inject(getElectronAppPathInjectable) }),
|
||||
|
||||
injectionToken: appPathsInjectionToken,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default appPathsInjectable;
|
||||
|
||||
const registerAppPathsChannel = (di: DependencyInjectionContainer) => {
|
||||
const registerChannel = di.inject(registerChannelInjectable);
|
||||
|
||||
registerChannel(appPathsIpcChannel, () => di.inject(appPathsInjectable));
|
||||
};
|
||||
|
||||
const setupPathForUserData = (di: DependencyInjectionContainer) => {
|
||||
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
|
||||
const appName = di.inject(appNameInjectable);
|
||||
const getAppPath = di.inject(getElectronAppPathInjectable);
|
||||
|
||||
const appDataPath = getAppPath("appData");
|
||||
|
||||
setElectronAppPath("userData", path.join(appDataPath, appName));
|
||||
};
|
||||
|
||||
const setupPathForAppDataInIntegrationTesting = (di: DependencyInjectionContainer, appDataPath: string) => {
|
||||
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
|
||||
|
||||
setElectronAppPath("appData", appDataPath);
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
|
||||
const directoryForIntegrationTestingInjectable = getInjectable({
|
||||
instantiate: () => process.env.CICD,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForIntegrationTestingInjectable;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { fromPairs } from "lodash/fp";
|
||||
import { pathNames, PathName } from "../../common/app-paths/app-path-names";
|
||||
import type { AppPaths } from "../../common/app-paths/app-path-injection-token";
|
||||
|
||||
interface Dependencies {
|
||||
getAppPath: (name: PathName) => string
|
||||
}
|
||||
|
||||
export const getAppPaths = ({ getAppPath }: Dependencies) =>
|
||||
fromPairs(pathNames.map((name) => [name, getAppPath(name)])) as AppPaths;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { app } from "electron";
|
||||
|
||||
const electronAppInjectable = getInjectable({
|
||||
instantiate: () => app,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
causesSideEffects: false,
|
||||
});
|
||||
|
||||
export default electronAppInjectable;
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import electronAppInjectable from "./electron-app/electron-app.injectable";
|
||||
import { getElectronAppPath } from "./get-electron-app-path";
|
||||
|
||||
const getElectronAppPathInjectable = getInjectable({
|
||||
instantiate: (di) =>
|
||||
getElectronAppPath({ app: di.inject(electronAppInjectable) }),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getElectronAppPathInjectable;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import electronAppInjectable from "./electron-app/electron-app.injectable";
|
||||
import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import type { App } from "electron";
|
||||
|
||||
describe("get-electron-app-path", () => {
|
||||
let getElectronAppPath: (name: string) => string | null;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
const appStub = {
|
||||
getPath: (name: string) => {
|
||||
if (name !== "some-existing-name") {
|
||||
throw new Error("irrelevant");
|
||||
}
|
||||
|
||||
return "some-existing-app-path";
|
||||
|
||||
},
|
||||
} as App;
|
||||
|
||||
di.override(electronAppInjectable, () => appStub);
|
||||
|
||||
getElectronAppPath = di.inject(getElectronAppPathInjectable);
|
||||
});
|
||||
|
||||
it("given app path exists, when called, returns app path", () => {
|
||||
const actual = getElectronAppPath("some-existing-name");
|
||||
|
||||
expect(actual).toBe("some-existing-app-path");
|
||||
});
|
||||
|
||||
it("given app path does not exist, when called, returns null", () => {
|
||||
const actual = getElectronAppPath("some-non-existing-name");
|
||||
|
||||
expect(actual).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { App } from "electron";
|
||||
import type { PathName } from "../../../common/app-paths/app-path-names";
|
||||
|
||||
interface Dependencies {
|
||||
app: App;
|
||||
}
|
||||
|
||||
export const getElectronAppPath =
|
||||
({ app }: Dependencies) =>
|
||||
(name: PathName) : string | null => {
|
||||
try {
|
||||
return app.getPath(name);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
const ipcMainInjectable = getInjectable({
|
||||
instantiate: () => ipcMain,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default ipcMainInjectable;
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import ipcMainInjectable from "./ipc-main/ipc-main.injectable";
|
||||
import { registerChannel } from "./register-channel";
|
||||
|
||||
const registerChannelInjectable = getInjectable({
|
||||
instantiate: (di) => registerChannel({
|
||||
ipcMain: di.inject(ipcMainInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default registerChannelInjectable;
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { IpcMain } from "electron";
|
||||
import type { Channel } from "../../../common/ipc-channel/channel";
|
||||
|
||||
interface Dependencies {
|
||||
ipcMain: IpcMain;
|
||||
}
|
||||
|
||||
export const registerChannel =
|
||||
({ ipcMain }: Dependencies) =>
|
||||
<TChannel extends Channel<TInstance>, TInstance>(
|
||||
channel: TChannel,
|
||||
getValue: () => TInstance,
|
||||
) =>
|
||||
ipcMain.handle(channel.name, getValue);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { PathName } from "../../../common/app-paths/app-path-names";
|
||||
import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable";
|
||||
|
||||
const setElectronAppPathInjectable = getInjectable({
|
||||
instantiate: (di) => (name: PathName, path: string) : void =>
|
||||
di.inject(electronAppInjectable).setPath(name, path),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default setElectronAppPathInjectable;
|
||||
@@ -22,13 +22,17 @@
|
||||
import { ObservableMap } from "mobx";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { loadFromOptions } from "../../../common/kube-helpers";
|
||||
import type { Cluster } from "../../cluster";
|
||||
import { computeDiff, configToModels } from "../kubeconfig-sync";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync-manager/kubeconfig-sync-manager";
|
||||
import mockFs from "mock-fs";
|
||||
import fs from "fs";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
import { AppPaths } from "../../../common/app-paths";
|
||||
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
||||
import directoryForKubeConfigsInjectable
|
||||
from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@@ -46,18 +50,28 @@ jest.mock("electron", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
describe("kubeconfig-sync.source tests", () => {
|
||||
beforeEach(() => {
|
||||
let computeDiff: ReturnType<typeof computeDiffFor>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs();
|
||||
ClusterStore.createInstance();
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
computeDiff = computeDiffFor({
|
||||
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
});
|
||||
|
||||
di.inject(clusterStoreInjectable);
|
||||
|
||||
ClusterManager.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
ClusterStore.resetInstance();
|
||||
ClusterManager.resetInstance();
|
||||
});
|
||||
|
||||
|
||||
@@ -20,5 +20,4 @@
|
||||
*/
|
||||
|
||||
export { syncWeblinks } from "./weblinks";
|
||||
export { KubeconfigSyncManager } from "./kubeconfig-sync";
|
||||
export { syncGeneralEntities } from "./general";
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import { KubeconfigSyncManager } from "./kubeconfig-sync-manager";
|
||||
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
||||
|
||||
const kubeconfigSyncManagerInjectable = getInjectable({
|
||||
instantiate: (di) => new KubeconfigSyncManager({
|
||||
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default kubeconfigSyncManagerInjectable;
|
||||
+40
-29
@@ -20,25 +20,25 @@
|
||||
*/
|
||||
|
||||
import { action, observable, IComputedValue, computed, ObservableMap, runInAction, makeObservable, observe } from "mobx";
|
||||
import type { CatalogEntity } from "../../common/catalog";
|
||||
import { catalogEntityRegistry } from "../../main/catalog";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { catalogEntityRegistry } from "../../catalog";
|
||||
import { FSWatcher, watch } from "chokidar";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type stream from "stream";
|
||||
import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop, Singleton, storedKubeConfigFolder } from "../../common/utils";
|
||||
import logger from "../logger";
|
||||
import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop } from "../../../common/utils";
|
||||
import logger from "../../logger";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { loadConfigFromString, splitConfig } from "../../common/kube-helpers";
|
||||
import { Cluster } from "../cluster";
|
||||
import { catalogEntityFromCluster, ClusterManager } from "../cluster-manager";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
|
||||
import { catalogEntityFromCluster, ClusterManager } from "../../cluster-manager";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { createHash } from "crypto";
|
||||
import { homedir } from "os";
|
||||
import globToRegExp from "glob-to-regexp";
|
||||
import { inspect } from "util";
|
||||
import type { UpdateClusterModel } from "../../common/cluster-types";
|
||||
import type { ClusterModel, UpdateClusterModel } from "../../../common/cluster-types";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
|
||||
const logPrefix = "[KUBECONFIG-SYNC]:";
|
||||
|
||||
@@ -63,16 +63,19 @@ const ignoreGlobs = [
|
||||
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
|
||||
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
|
||||
|
||||
export class KubeconfigSyncManager extends Singleton {
|
||||
interface Dependencies {
|
||||
directoryForKubeConfigs: string
|
||||
createCluster: (model: ClusterModel) => Cluster
|
||||
}
|
||||
|
||||
const kubeConfigSyncName = "lens:kube-sync";
|
||||
|
||||
export class KubeconfigSyncManager {
|
||||
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
||||
protected syncing = false;
|
||||
protected syncListDisposer?: Disposer;
|
||||
|
||||
protected static readonly syncName = "lens:kube-sync";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@@ -86,7 +89,7 @@ export class KubeconfigSyncManager extends Singleton {
|
||||
|
||||
logger.info(`${logPrefix} starting requested syncs`);
|
||||
|
||||
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => (
|
||||
catalogEntityRegistry.addComputedSource(kubeConfigSyncName, computed(() => (
|
||||
Array.from(iter.flatMap(
|
||||
this.sources.values(),
|
||||
([entities]) => entities.get(),
|
||||
@@ -94,7 +97,7 @@ export class KubeconfigSyncManager extends Singleton {
|
||||
)));
|
||||
|
||||
// This must be done so that c&p-ed clusters are visible
|
||||
this.startNewSync(storedKubeConfigFolder());
|
||||
this.startNewSync(this.dependencies.directoryForKubeConfigs);
|
||||
|
||||
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
|
||||
this.startNewSync(filePath);
|
||||
@@ -120,7 +123,7 @@ export class KubeconfigSyncManager extends Singleton {
|
||||
this.stopOldSync(filePath);
|
||||
}
|
||||
|
||||
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
|
||||
catalogEntityRegistry.removeSource(kubeConfigSyncName);
|
||||
this.syncing = false;
|
||||
}
|
||||
|
||||
@@ -131,7 +134,11 @@ export class KubeconfigSyncManager extends Singleton {
|
||||
return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath });
|
||||
}
|
||||
|
||||
this.sources.set(filePath, watchFileChanges(filePath));
|
||||
this.sources.set(
|
||||
filePath,
|
||||
watchFileChanges(filePath, this.dependencies),
|
||||
);
|
||||
|
||||
logger.info(`${logPrefix} starting sync of file/folder`, { filePath });
|
||||
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
|
||||
}
|
||||
@@ -170,7 +177,7 @@ type RootSourceValue = [Cluster, CatalogEntity];
|
||||
type RootSource = ObservableMap<string, RootSourceValue>;
|
||||
|
||||
// exported for testing
|
||||
export function computeDiff(contents: string, source: RootSource, filePath: string): void {
|
||||
export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Dependencies) => (contents: string, source: RootSource, filePath: string): void => {
|
||||
runInAction(() => {
|
||||
try {
|
||||
const { config, error } = loadConfigFromString(contents);
|
||||
@@ -212,7 +219,8 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
||||
// add new clusters to the source
|
||||
try {
|
||||
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId });
|
||||
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId) || createCluster({ ...model, id: clusterId });
|
||||
|
||||
if (!cluster.apiUrl) {
|
||||
throw new Error("Cluster constructor failed, see above error");
|
||||
@@ -220,7 +228,7 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
||||
|
||||
const entity = catalogEntityFromCluster(cluster);
|
||||
|
||||
if (!filePath.startsWith(storedKubeConfigFolder())) {
|
||||
if (!filePath.startsWith(directoryForKubeConfigs)) {
|
||||
entity.metadata.labels.file = filePath.replace(homedir(), "~");
|
||||
}
|
||||
source.set(contextName, [cluster, entity]);
|
||||
@@ -231,11 +239,12 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
|
||||
source.clear(); // clear source if we have failed so as to not show outdated information
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
interface DiffChangedConfigArgs {
|
||||
filePath: string;
|
||||
@@ -244,7 +253,7 @@ interface DiffChangedConfigArgs {
|
||||
maxAllowedFileReadSize: number;
|
||||
}
|
||||
|
||||
function diffChangedConfig({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer {
|
||||
const diffChangedConfigFor = (dependencies: Dependencies) => ({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer => {
|
||||
logger.debug(`${logPrefix} file changed`, { filePath });
|
||||
|
||||
if (stats.size >= maxAllowedFileReadSize) {
|
||||
@@ -293,14 +302,14 @@ function diffChangedConfig({ filePath, source, stats, maxAllowedFileReadSize }:
|
||||
})
|
||||
.on("end", () => {
|
||||
if (!closed) {
|
||||
computeDiff(fileString, source, filePath);
|
||||
computeDiff(dependencies)(fileString, source, filePath);
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}
|
||||
};
|
||||
|
||||
function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, Disposer] {
|
||||
const watchFileChanges = (filePath: string, dependencies: Dependencies): [IComputedValue<CatalogEntity[]>, Disposer] => {
|
||||
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
|
||||
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
|
||||
|
||||
@@ -328,6 +337,8 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
|
||||
atomic: 150, // for "atomic writes"
|
||||
});
|
||||
|
||||
const diffChangedConfig = diffChangedConfigFor(dependencies);
|
||||
|
||||
watcher
|
||||
.on("change", (childFilePath, stats) => {
|
||||
const cleanup = cleanupFns.get(childFilePath);
|
||||
@@ -378,4 +389,4 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
|
||||
return [derivedSource, () => {
|
||||
watcher?.close();
|
||||
}];
|
||||
}
|
||||
};
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import type { RequestPromiseOptions } from "request-promise-native";
|
||||
import type { Cluster } from "../cluster";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { k8sRequest } from "../k8s-request";
|
||||
|
||||
export type ClusterDetectionResult = {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import { observable } from "mobx";
|
||||
import type { ClusterMetadata } from "../../common/cluster-types";
|
||||
import { Singleton } from "../../common/utils";
|
||||
import type { Cluster } from "../cluster";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
||||
|
||||
export class DetectorRegistry extends Singleton {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import "../common/cluster-ipc";
|
||||
import type http from "http";
|
||||
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||
import { Cluster } from "./cluster";
|
||||
import { Cluster } from "../common/cluster/cluster";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { getClusterIdFromHost, Singleton } from "../common/utils";
|
||||
@@ -30,7 +30,7 @@ import { catalogEntityRegistry } from "./catalog";
|
||||
import { KubernetesCluster, KubernetesClusterPrometheusMetrics, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { once } from "lodash";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
import { ClusterStore } from "../common/cluster-store/cluster-store";
|
||||
import type { ClusterId } from "../common/cluster-types";
|
||||
|
||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
|
||||
@@ -1,724 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { ipcMain } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../common/kube-helpers";
|
||||
import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../common/rbac";
|
||||
import logger from "./logger";
|
||||
import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||
import { DetectorRegistry } from "./cluster-detectors/detector-registry";
|
||||
import plimit from "p-limit";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types";
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
|
||||
import { disposer, storedKubeConfigFolder, toJS } from "../common/utils";
|
||||
import type { Response } from "request";
|
||||
|
||||
/**
|
||||
* Cluster
|
||||
*
|
||||
* @beta
|
||||
*/
|
||||
export class Cluster implements ClusterModel, ClusterState {
|
||||
/** Unique id for a cluster */
|
||||
public readonly id: ClusterId;
|
||||
private kubeCtl: Kubectl;
|
||||
/**
|
||||
* Context handler
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public contextHandler: ContextHandler;
|
||||
protected proxyKubeconfigManager: KubeconfigManager;
|
||||
protected eventsDisposer = disposer();
|
||||
protected activated = false;
|
||||
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
||||
|
||||
get whenReady() {
|
||||
return when(() => this.ready);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kubeconfig context name
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable contextName: string;
|
||||
/**
|
||||
* Path to kubeconfig
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable kubeConfigPath: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@observable workspace: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@observable workspaces: string[];
|
||||
/**
|
||||
* Kubernetes API server URL
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable apiUrl: string; // cluster server url
|
||||
/**
|
||||
* Is cluster online
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable online = false; // describes if we can detect that cluster is online
|
||||
/**
|
||||
* Can user access cluster resources
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable accessible = false; // if user is able to access cluster resources
|
||||
/**
|
||||
* Is cluster instance in usable state
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable ready = false; // cluster is in usable state
|
||||
/**
|
||||
* Is cluster currently reconnecting
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable reconnecting = false;
|
||||
/**
|
||||
* Is cluster disconnected. False if user has selected to connect.
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable disconnected = true;
|
||||
/**
|
||||
* Does user have admin like access
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable isAdmin = false;
|
||||
|
||||
/**
|
||||
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable isGlobalWatchEnabled = false;
|
||||
/**
|
||||
* Preferences
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable preferences: ClusterPreferences = {};
|
||||
/**
|
||||
* Metadata
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable metadata: ClusterMetadata = {};
|
||||
/**
|
||||
* List of allowed namespaces verified via K8S::SelfSubjectAccessReview api
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable allowedNamespaces: string[] = [];
|
||||
/**
|
||||
* List of allowed resources
|
||||
*
|
||||
* @observable
|
||||
* @internal
|
||||
*/
|
||||
@observable allowedResources: string[] = [];
|
||||
/**
|
||||
* List of accessible namespaces provided by user in the Cluster Settings
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable accessibleNamespaces: string[] = [];
|
||||
|
||||
/**
|
||||
* Labels for the catalog entity
|
||||
*/
|
||||
@observable labels: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Is cluster available
|
||||
*
|
||||
* @computed
|
||||
*/
|
||||
@computed get available() {
|
||||
return this.accessible && !this.disconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cluster name
|
||||
*
|
||||
* @computed
|
||||
*/
|
||||
@computed get name() {
|
||||
return this.preferences.clusterName || this.contextName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The detected kubernetes distribution
|
||||
*/
|
||||
@computed get distribution(): string {
|
||||
return this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* The detected kubernetes version
|
||||
*/
|
||||
@computed get version(): string {
|
||||
return this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prometheus preferences
|
||||
*
|
||||
* @computed
|
||||
* @internal
|
||||
*/
|
||||
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
||||
const { prometheus, prometheusProvider } = this.preferences;
|
||||
|
||||
return toJS({ prometheus, prometheusProvider });
|
||||
}
|
||||
|
||||
/**
|
||||
* defaultNamespace preference
|
||||
*
|
||||
* @computed
|
||||
* @internal
|
||||
*/
|
||||
@computed get defaultNamespace(): string {
|
||||
return this.preferences.defaultNamespace;
|
||||
}
|
||||
|
||||
constructor(model: ClusterModel) {
|
||||
makeObservable(this);
|
||||
this.id = model.id;
|
||||
this.updateModel(model);
|
||||
|
||||
const { config } = loadConfigFromFileSync(this.kubeConfigPath);
|
||||
const validationError = validateKubeConfig(config, this.contextName);
|
||||
|
||||
if (validationError) {
|
||||
throw validationError;
|
||||
}
|
||||
|
||||
this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server;
|
||||
|
||||
if (ipcMain) {
|
||||
// for the time being, until renderer gets its own cluster type
|
||||
this.contextHandler = new ContextHandler(this);
|
||||
this.proxyKubeconfigManager = new KubeconfigManager(this, this.contextHandler);
|
||||
|
||||
logger.debug(`[CLUSTER]: Cluster init success`, {
|
||||
id: this.id,
|
||||
context: this.contextName,
|
||||
apiUrl: this.apiUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cluster data model
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
@action updateModel(model: UpdateClusterModel) {
|
||||
// Note: do not assign ID as that should never be updated
|
||||
|
||||
this.kubeConfigPath = model.kubeConfigPath;
|
||||
|
||||
if (model.workspace) {
|
||||
this.workspace = model.workspace;
|
||||
}
|
||||
|
||||
if (model.workspaces) {
|
||||
this.workspaces = model.workspaces;
|
||||
}
|
||||
|
||||
if (model.contextName) {
|
||||
this.contextName = model.contextName;
|
||||
}
|
||||
|
||||
if (model.preferences) {
|
||||
this.preferences = model.preferences;
|
||||
}
|
||||
|
||||
if (model.metadata) {
|
||||
this.metadata = model.metadata;
|
||||
}
|
||||
|
||||
if (model.accessibleNamespaces) {
|
||||
this.accessibleNamespaces = model.accessibleNamespaces;
|
||||
}
|
||||
|
||||
if (model.labels) {
|
||||
this.labels = model.labels;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
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
|
||||
|
||||
this.eventsDisposer.push(
|
||||
reaction(() => this.getState(), state => this.pushState(state)),
|
||||
reaction(
|
||||
() => this.prometheusPreferences,
|
||||
prefs => this.contextHandler.setupPrometheus(prefs),
|
||||
{ equals: comparer.structural },
|
||||
),
|
||||
() => clearInterval(refreshTimer),
|
||||
() => clearInterval(refreshMetadataTimer),
|
||||
reaction(() => this.defaultNamespace, () => this.recreateProxyKubeconfig()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async recreateProxyKubeconfig() {
|
||||
logger.info("[CLUSTER]: Recreating proxy kubeconfig");
|
||||
|
||||
try {
|
||||
await this.proxyKubeconfigManager.clear();
|
||||
await this.getProxyKubeconfig();
|
||||
} catch (error) {
|
||||
logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param force force activation
|
||||
* @internal
|
||||
*/
|
||||
@action
|
||||
async activate(force = false) {
|
||||
if (this.activated && !force) {
|
||||
return this.pushState();
|
||||
}
|
||||
|
||||
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||
|
||||
if (!this.eventsDisposer.length) {
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
if (this.disconnected || !this.accessible) {
|
||||
await this.reconnect();
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate("Refreshing connection status ...");
|
||||
await this.refreshConnectionStatus();
|
||||
|
||||
if (this.accessible) {
|
||||
this.broadcastConnectUpdate("Refreshing cluster accessibility ...");
|
||||
await this.refreshAccessibility();
|
||||
// download kubectl in background, so it's not blocking dashboard
|
||||
this.ensureKubectl()
|
||||
.catch(error => logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error));
|
||||
this.broadcastConnectUpdate("Connected, waiting for view to load ...");
|
||||
}
|
||||
|
||||
this.activated = true;
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async ensureKubectl() {
|
||||
this.kubeCtl ??= new Kubectl(this.version);
|
||||
|
||||
await this.kubeCtl.ensureKubectl();
|
||||
|
||||
return this.kubeCtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@action
|
||||
async reconnect() {
|
||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||
this.contextHandler?.stopServer();
|
||||
await this.contextHandler?.ensureServer();
|
||||
this.disconnected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@action disconnect() {
|
||||
logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
|
||||
this.eventsDisposer();
|
||||
this.contextHandler?.stopServer();
|
||||
this.disconnected = true;
|
||||
this.online = false;
|
||||
this.accessible = false;
|
||||
this.ready = false;
|
||||
this.activated = false;
|
||||
this.allowedNamespaces = [];
|
||||
this.resourceAccessStatuses.clear();
|
||||
this.pushState();
|
||||
logger.info(`[CLUSTER]: disconnected`, { id: this.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param opts refresh options
|
||||
*/
|
||||
@action
|
||||
async refresh(opts: ClusterRefreshOptions = {}) {
|
||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||
await this.refreshConnectionStatus();
|
||||
|
||||
if (this.accessible) {
|
||||
await this.refreshAccessibility();
|
||||
|
||||
if (opts.refreshMetadata) {
|
||||
this.refreshMetadata();
|
||||
}
|
||||
}
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@action
|
||||
async refreshMetadata() {
|
||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||
const metadata = await DetectorRegistry.getInstance().detectForCluster(this);
|
||||
const existingMetadata = this.metadata;
|
||||
|
||||
this.metadata = Object.assign(existingMetadata, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private async refreshAccessibility(): Promise<void> {
|
||||
this.isAdmin = await this.isClusterAdmin();
|
||||
this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
|
||||
this.allowedNamespaces = await this.getAllowedNamespaces();
|
||||
this.allowedResources = await this.getAllowedResources();
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@action
|
||||
async refreshConnectionStatus() {
|
||||
const connectionStatus = await this.getConnectionStatus();
|
||||
|
||||
this.online = connectionStatus > ClusterStatus.Offline;
|
||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||
}
|
||||
|
||||
async getKubeconfig(): Promise<KubeConfig> {
|
||||
const { config } = await loadConfigFromFile(this.kubeConfigPath);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async getProxyKubeconfig(): Promise<KubeConfig> {
|
||||
const proxyKCPath = await this.getProxyKubeconfigPath();
|
||||
const { config } = await loadConfigFromFile(proxyKCPath);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async getProxyKubeconfigPath(): Promise<string> {
|
||||
return this.proxyKubeconfigManager.getPath();
|
||||
}
|
||||
|
||||
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
||||
try {
|
||||
const versionDetector = new VersionDetector(this);
|
||||
const versionData = await versionDetector.detect();
|
||||
|
||||
this.metadata.version = versionData.value;
|
||||
|
||||
return ClusterStatus.AccessGranted;
|
||||
} catch (error) {
|
||||
logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
|
||||
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.broadcastConnectUpdate("Invalid credentials", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate(error.error || error.message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.broadcastConnectUpdate("Connection timed out", true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate("Failed to fetch credentials", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate(error.message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param resourceAttributes resource attributes
|
||||
*/
|
||||
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||
const authApi = (await this.getProxyKubeconfig()).makeApiClient(AuthorizationV1Api);
|
||||
|
||||
try {
|
||||
const accessReview = await authApi.createSelfSubjectAccessReview({
|
||||
apiVersion: "authorization.k8s.io/v1",
|
||||
kind: "SelfSubjectAccessReview",
|
||||
spec: { resourceAttributes },
|
||||
});
|
||||
|
||||
return accessReview.body.status.allowed;
|
||||
} catch (error) {
|
||||
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async isClusterAdmin(): Promise<boolean> {
|
||||
return this.canI({
|
||||
namespace: "kube-system",
|
||||
resource: "*",
|
||||
verb: "create",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
|
||||
return this.canI({
|
||||
verb: "watch",
|
||||
resource: "*",
|
||||
...customizeResource,
|
||||
});
|
||||
}
|
||||
|
||||
toJSON(): ClusterModel {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
contextName: this.contextName,
|
||||
kubeConfigPath: this.kubeConfigPath,
|
||||
workspace: this.workspace,
|
||||
workspaces: this.workspaces,
|
||||
preferences: this.preferences,
|
||||
metadata: this.metadata,
|
||||
accessibleNamespaces: this.accessibleNamespaces,
|
||||
labels: this.labels,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable cluster-state used for sync btw main <-> renderer
|
||||
*/
|
||||
getState(): ClusterState {
|
||||
return toJS({
|
||||
apiUrl: this.apiUrl,
|
||||
online: this.online,
|
||||
ready: this.ready,
|
||||
disconnected: this.disconnected,
|
||||
accessible: this.accessible,
|
||||
isAdmin: this.isAdmin,
|
||||
allowedNamespaces: this.allowedNamespaces,
|
||||
allowedResources: this.allowedResources,
|
||||
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param state cluster state
|
||||
*/
|
||||
@action setState(state: ClusterState) {
|
||||
Object.assign(this, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param state cluster state
|
||||
*/
|
||||
pushState(state = this.getState()) {
|
||||
logger.silly(`[CLUSTER]: push-state`, state);
|
||||
broadcastMessage("cluster:state", this.id, state);
|
||||
}
|
||||
|
||||
// get cluster system meta, e.g. use in "logger"
|
||||
getMeta() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.contextName,
|
||||
ready: this.ready,
|
||||
online: this.online,
|
||||
accessible: this.accessible,
|
||||
disconnected: this.disconnected,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* broadcast an authentication update concerning this cluster
|
||||
* @internal
|
||||
*/
|
||||
broadcastConnectUpdate(message: string, isError = false): void {
|
||||
const update: KubeAuthUpdate = { message, isError };
|
||||
|
||||
logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() });
|
||||
broadcastMessage(`cluster:${this.id}:connection-update`, update);
|
||||
}
|
||||
|
||||
protected async getAllowedNamespaces() {
|
||||
if (this.accessibleNamespaces.length) {
|
||||
return this.accessibleNamespaces;
|
||||
}
|
||||
|
||||
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
|
||||
try {
|
||||
const { body: { items }} = await api.listNamespace();
|
||||
const namespaces = items.map(ns => ns.metadata.name);
|
||||
|
||||
return namespaces;
|
||||
} catch (error) {
|
||||
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
|
||||
const namespaceList = [ctx.namespace].filter(Boolean);
|
||||
|
||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||
const { response } = error as HttpError & { response: Response };
|
||||
|
||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
|
||||
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
|
||||
}
|
||||
|
||||
return namespaceList;
|
||||
}
|
||||
}
|
||||
|
||||
protected async getAllowedResources() {
|
||||
try {
|
||||
if (!this.allowedNamespaces.length) {
|
||||
return [];
|
||||
}
|
||||
const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined);
|
||||
const apiLimit = plimit(5); // 5 concurrent api requests
|
||||
const requests = [];
|
||||
|
||||
for (const apiResource of resources) {
|
||||
requests.push(apiLimit(async () => {
|
||||
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
||||
if (!this.resourceAccessStatuses.get(apiResource)) {
|
||||
const result = await this.canI({
|
||||
resource: apiResource.apiName,
|
||||
group: apiResource.group,
|
||||
verb: "list",
|
||||
namespace,
|
||||
});
|
||||
|
||||
this.resourceAccessStatuses.set(apiResource, result);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
await Promise.all(requests);
|
||||
|
||||
return apiResources
|
||||
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
||||
.map(apiResource => apiResource.apiName);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
isAllowedResource(kind: string): boolean {
|
||||
if ((kind as KubeResource) in apiResourceRecord) {
|
||||
return this.allowedResources.includes(kind);
|
||||
}
|
||||
|
||||
const apiResource = apiResources.find(resource => resource.kind === kind);
|
||||
|
||||
if (apiResource) {
|
||||
return this.allowedResources.includes(apiResource.apiName);
|
||||
}
|
||||
|
||||
return true; // allowed by default for other resources
|
||||
}
|
||||
|
||||
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
||||
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
||||
}
|
||||
|
||||
get nodeShellImage(): string {
|
||||
return this.preferences?.nodeShellImage || initialNodeShellImage;
|
||||
}
|
||||
|
||||
get imagePullSecret(): string | undefined {
|
||||
return this.preferences?.imagePullSecret;
|
||||
}
|
||||
|
||||
isInLocalKubeconfig() {
|
||||
return this.kubeConfigPath.startsWith(storedKubeConfigFolder());
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,15 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry";
|
||||
import { PrometheusProviderRegistry } from "./prometheus/provider-registry";
|
||||
import type { ClusterPrometheusPreferences } from "../common/cluster-types";
|
||||
import type { Cluster } from "./cluster";
|
||||
import type { PrometheusProvider, PrometheusService } from "../prometheus/provider-registry";
|
||||
import { PrometheusProviderRegistry } from "../prometheus/provider-registry";
|
||||
import type { ClusterPrometheusPreferences } from "../../common/cluster-types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type httpProxy from "http-proxy";
|
||||
import url, { UrlWithStringQuery } from "url";
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import logger from "./logger";
|
||||
import { KubeAuthProxy } from "./kube-auth-proxy";
|
||||
import logger from "../logger";
|
||||
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
|
||||
|
||||
export interface PrometheusDetails {
|
||||
prometheusPath: string;
|
||||
@@ -41,6 +41,10 @@ interface PrometheusServicePreferences {
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy
|
||||
}
|
||||
|
||||
export class ContextHandler {
|
||||
public clusterUrl: UrlWithStringQuery;
|
||||
protected kubeAuthProxy?: KubeAuthProxy;
|
||||
@@ -48,7 +52,7 @@ export class ContextHandler {
|
||||
protected prometheusProvider?: string;
|
||||
protected prometheus?: PrometheusServicePreferences;
|
||||
|
||||
constructor(protected cluster: Cluster) {
|
||||
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
|
||||
this.clusterUrl = url.parse(cluster.apiUrl);
|
||||
this.setupPrometheus(cluster.preferences);
|
||||
}
|
||||
@@ -161,7 +165,7 @@ export class ContextHandler {
|
||||
if (this.cluster.preferences.httpsProxy) {
|
||||
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
||||
}
|
||||
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, proxyEnv);
|
||||
this.kubeAuthProxy = this.dependencies.createKubeAuthProxy(this.cluster, proxyEnv);
|
||||
await this.kubeAuthProxy.run();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
|
||||
const createContextHandlerInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const dependencies = {
|
||||
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
|
||||
};
|
||||
|
||||
return (cluster: Cluster) => new ContextHandler(dependencies, cluster);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createContextHandlerInjectable;
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { ClusterModel } from "../../common/cluster-types";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||
import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
|
||||
const createClusterInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const dependencies = {
|
||||
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
|
||||
createKubeconfigManager: di.inject(createKubeconfigManagerInjectable),
|
||||
createKubectl: di.inject(createKubectlInjectable),
|
||||
createContextHandler: di.inject(createContextHandlerInjectable),
|
||||
};
|
||||
|
||||
return (model: ClusterModel) => new Cluster(dependencies, model);
|
||||
},
|
||||
|
||||
injectionToken: createClusterInjectionToken,
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createClusterInjectable;
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import { app } from "electron";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
import logger from "./logger";
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { randomBytes } from "crypto";
|
||||
import { SHA256 } from "crypto-js";
|
||||
import fse from "fs-extra";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import path from "path";
|
||||
import { BaseStore } from "../common/base-store";
|
||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||
import { toJS } from "../common/utils";
|
||||
import { AppPaths } from "../common/app-paths";
|
||||
|
||||
interface FSProvisionModel {
|
||||
extensions: Record<string, string>; // extension names to paths
|
||||
}
|
||||
|
||||
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
||||
readonly displayName = "FilesystemProvisionerStore";
|
||||
registeredExtensions = observable.map<LensExtensionId, string>();
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
configName: "lens-filesystem-provisioner-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
});
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function retrieves the saved path to the folder which the extension
|
||||
* can saves files to. If the folder is not present then it is created.
|
||||
* @param extensionName the name of the extension requesting the path
|
||||
* @returns path to the folder that the extension can safely write files to.
|
||||
*/
|
||||
async requestDirectory(extensionName: string): Promise<string> {
|
||||
if (!this.registeredExtensions.has(extensionName)) {
|
||||
const salt = randomBytes(32).toString("hex");
|
||||
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
||||
const dirPath = path.resolve(AppPaths.get("userData"), "extension_data", hashedName);
|
||||
|
||||
this.registeredExtensions.set(extensionName, dirPath);
|
||||
}
|
||||
|
||||
const dirPath = this.registeredExtensions.get(extensionName);
|
||||
|
||||
await fse.ensureDir(dirPath);
|
||||
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore({ extensions }: FSProvisionModel = { extensions: {}}): void {
|
||||
this.registeredExtensions.merge(extensions);
|
||||
}
|
||||
|
||||
toJSON(): FSProvisionModel {
|
||||
return toJS({
|
||||
extensions: Object.fromEntries(this.registeredExtensions),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export const getDi = () => {
|
||||
const di = createContainer(
|
||||
getRequireContextForMainCode,
|
||||
getRequireContextForCommonExtensionCode,
|
||||
getRequireContextForCommonCode,
|
||||
);
|
||||
|
||||
setLegacyGlobalDiForExtensionApi(di);
|
||||
@@ -38,3 +39,6 @@ const getRequireContextForMainCode = () =>
|
||||
|
||||
const getRequireContextForCommonExtensionCode = () =>
|
||||
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
const getRequireContextForCommonCode = () =>
|
||||
require.context("../common", true, /\.injectable\.(ts|tsx)$/);
|
||||
@@ -21,20 +21,32 @@
|
||||
|
||||
import glob from "glob";
|
||||
import { memoize } from "lodash/fp";
|
||||
import { kebabCase } from "lodash/fp";
|
||||
|
||||
import {
|
||||
createContainer,
|
||||
ConfigurableDependencyInjectionContainer,
|
||||
} from "@ogre-tools/injectable";
|
||||
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
|
||||
|
||||
export const getDiForUnitTesting = () => {
|
||||
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable";
|
||||
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
|
||||
import appNameInjectable from "./app-paths/app-name/app-name.injectable";
|
||||
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
|
||||
import writeJsonFileInjectable
|
||||
from "../common/fs/write-json-file/write-json-file.injectable";
|
||||
import readJsonFileInjectable
|
||||
from "../common/fs/read-json-file/read-json-file.injectable";
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
) => {
|
||||
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
||||
|
||||
setLegacyGlobalDiForExtensionApi(di);
|
||||
|
||||
getInjectableFilePaths()
|
||||
.map(key => {
|
||||
.map((key) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const injectable = require(key).default;
|
||||
|
||||
@@ -45,14 +57,34 @@ export const getDiForUnitTesting = () => {
|
||||
};
|
||||
})
|
||||
|
||||
.forEach(injectable => di.register(injectable));
|
||||
.forEach((injectable) => di.register(injectable));
|
||||
|
||||
di.preventSideEffects();
|
||||
|
||||
if (doGeneralOverrides) {
|
||||
di.override(
|
||||
getElectronAppPathInjectable,
|
||||
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
|
||||
);
|
||||
|
||||
di.override(setElectronAppPathInjectable, () => () => undefined);
|
||||
di.override(appNameInjectable, () => "some-electron-app-name");
|
||||
di.override(registerChannelInjectable, () => () => undefined);
|
||||
|
||||
di.override(writeJsonFileInjectable, () => () => {
|
||||
throw new Error("Tried to write JSON file to file system without specifying explicit override.");
|
||||
});
|
||||
|
||||
di.override(readJsonFileInjectable, () => () => {
|
||||
throw new Error("Tried to read JSON file from file system without specifying explicit override.");
|
||||
});
|
||||
}
|
||||
|
||||
return di;
|
||||
};
|
||||
|
||||
const getInjectableFilePaths = memoize(() => [
|
||||
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||
]);
|
||||
@@ -19,7 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../cluster";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import logger from "../logger";
|
||||
import { HelmRepoManager } from "./helm-repo-manager";
|
||||
import { HelmChartManager } from "./helm-chart-manager";
|
||||
|
||||
+258
-232
@@ -35,315 +35,343 @@ import { shellSync } from "./shell-sync";
|
||||
import { mangleProxyEnv } from "./proxy-env";
|
||||
import { registerFileProtocol } from "../common/register-protocol";
|
||||
import logger from "./logger";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import type { InstalledExtension } from "../extensions/extension-discovery/extension-discovery";
|
||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||
import { installDeveloperTools } from "./developer-tools";
|
||||
import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils";
|
||||
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||
import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc";
|
||||
import { startUpdateChecking } from "./app-updater";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import { pushCatalogToRenderer } from "./catalog-pusher";
|
||||
import { catalogEntityRegistry } from "./catalog";
|
||||
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
||||
import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources";
|
||||
import { syncGeneralEntities, syncWeblinks } from "./catalog-sources";
|
||||
import configurePackages from "../common/configure-packages";
|
||||
import { PrometheusProviderRegistry } from "./prometheus";
|
||||
import * as initializers from "./initializers";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
import { HotbarStore } from "../common/hotbar-store";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import { WeblinkStore } from "../common/weblink-store";
|
||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||
import { SentryInit } from "../common/sentry";
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { Router } from "./router";
|
||||
import { initMenu } from "./menu/menu";
|
||||
import { initTray } from "./tray";
|
||||
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
|
||||
import { AppPaths } from "../common/app-paths";
|
||||
import { kubeApiRequest } from "./proxy-functions";
|
||||
import { ShellSession } from "./shell-session/shell-session";
|
||||
import { getDi } from "./getDi";
|
||||
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||
import extensionDiscoveryInjectable
|
||||
from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import directoryForExesInjectable
|
||||
from "../common/app-paths/directory-for-exes/directory-for-exes.injectable";
|
||||
import initIpcMainHandlersInjectable
|
||||
from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable";
|
||||
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
||||
import directoryForKubeConfigsInjectable
|
||||
from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import kubeconfigSyncManagerInjectable
|
||||
from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable";
|
||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||
import routerInjectable from "./router/router.injectable";
|
||||
import shellApiRequestInjectable
|
||||
from "./proxy-functions/shell-api-request/shell-api-request.injectable";
|
||||
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
||||
|
||||
const di = getDi();
|
||||
|
||||
injectSystemCAs();
|
||||
di.runSetups().then(() => {
|
||||
injectSystemCAs();
|
||||
|
||||
const onCloseCleanup = disposer();
|
||||
const onQuitCleanup = disposer();
|
||||
const onCloseCleanup = disposer();
|
||||
const onQuitCleanup = disposer();
|
||||
|
||||
SentryInit();
|
||||
app.setName(appName);
|
||||
SentryInit();
|
||||
app.setName(appName);
|
||||
|
||||
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
||||
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
||||
|
||||
if (app.setAsDefaultProtocolClient("lens")) {
|
||||
logger.info("📟 Protocol client register succeeded ✅");
|
||||
} else {
|
||||
logger.info("📟 Protocol client register failed ❗");
|
||||
}
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
if (process.env.LENS_DISABLE_GPU) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
logger.debug("[APP-MAIN] initializing remote");
|
||||
initializeRemote();
|
||||
|
||||
logger.debug("[APP-MAIN] configuring packages");
|
||||
configurePackages();
|
||||
|
||||
mangleProxyEnv();
|
||||
|
||||
logger.debug("[APP-MAIN] initializing ipc main handlers");
|
||||
|
||||
const menuItems = di.inject(electronMenuItemsInjectable);
|
||||
|
||||
initializers.initIpcMainHandlers(menuItems);
|
||||
|
||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
||||
}
|
||||
|
||||
logger.debug("[APP-MAIN] Lens protocol routing main");
|
||||
|
||||
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
} else {
|
||||
for (const arg of process.argv) {
|
||||
if (arg.toLowerCase().startsWith("lens://")) {
|
||||
lensProtocolRouterMain.route(arg);
|
||||
}
|
||||
if (app.setAsDefaultProtocolClient("lens")) {
|
||||
logger.info("📟 Protocol client register succeeded ✅");
|
||||
} else {
|
||||
logger.info("📟 Protocol client register failed ❗");
|
||||
}
|
||||
}
|
||||
|
||||
app.on("second-instance", (event, argv) => {
|
||||
logger.debug("second-instance message");
|
||||
if (process.env.LENS_DISABLE_GPU) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg.toLowerCase().startsWith("lens://")) {
|
||||
lensProtocolRouterMain.route(arg);
|
||||
logger.debug("[APP-MAIN] initializing remote");
|
||||
initializeRemote();
|
||||
|
||||
logger.debug("[APP-MAIN] configuring packages");
|
||||
configurePackages();
|
||||
|
||||
mangleProxyEnv();
|
||||
|
||||
const initIpcMainHandlers = di.inject(initIpcMainHandlersInjectable);
|
||||
|
||||
logger.debug("[APP-MAIN] initializing ipc main handlers");
|
||||
initIpcMainHandlers();
|
||||
|
||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
||||
}
|
||||
|
||||
logger.debug("[APP-MAIN] Lens protocol routing main");
|
||||
|
||||
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
} else {
|
||||
for (const arg of process.argv) {
|
||||
if (arg.toLowerCase().startsWith("lens://")) {
|
||||
lensProtocolRouterMain.route(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowManager.getInstance(false)?.ensureMainWindow();
|
||||
});
|
||||
app.on("second-instance", (event, argv) => {
|
||||
logger.debug("second-instance message");
|
||||
|
||||
app.on("ready", async () => {
|
||||
logger.info(`🚀 Starting ${productName} from "${AppPaths.get("exe")}"`);
|
||||
logger.info("🐚 Syncing shell environment");
|
||||
await shellSync();
|
||||
for (const arg of argv) {
|
||||
if (arg.toLowerCase().startsWith("lens://")) {
|
||||
lensProtocolRouterMain.route(arg);
|
||||
}
|
||||
}
|
||||
|
||||
bindBroadcastHandlers();
|
||||
|
||||
powerMonitor.on("shutdown", () => app.exit());
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
PrometheusProviderRegistry.createInstance();
|
||||
ShellRequestAuthenticator.createInstance().init();
|
||||
initializers.initPrometheusProviderRegistry();
|
||||
|
||||
/**
|
||||
* The following sync MUST be done before HotbarStore creation, because that
|
||||
* store has migrations that will remove items that previous migrations add
|
||||
* if this is not present
|
||||
*/
|
||||
syncGeneralEntities();
|
||||
|
||||
logger.info("💾 Loading stores");
|
||||
|
||||
UserStore.createInstance().startMainReactions();
|
||||
|
||||
// ClusterStore depends on: UserStore
|
||||
ClusterStore.createInstance().provideInitialFromMain();
|
||||
|
||||
// HotbarStore depends on: ClusterStore
|
||||
HotbarStore.createInstance();
|
||||
|
||||
FilesystemProvisionerStore.createInstance();
|
||||
WeblinkStore.createInstance();
|
||||
|
||||
syncWeblinks();
|
||||
|
||||
HelmRepoManager.createInstance(); // create the instance
|
||||
|
||||
const lensProxy = LensProxy.createInstance(new Router(), {
|
||||
getClusterForRequest: req => ClusterManager.getInstance().getClusterForRequest(req),
|
||||
kubeApiRequest,
|
||||
shellApiRequest,
|
||||
WindowManager.getInstance(false)?.ensureMainWindow();
|
||||
});
|
||||
|
||||
ClusterManager.createInstance().init();
|
||||
KubeconfigSyncManager.createInstance();
|
||||
app.on("ready", async () => {
|
||||
const directoryForExes = di.inject(directoryForExesInjectable);
|
||||
|
||||
initializers.initClusterMetadataDetectors();
|
||||
logger.info(`🚀 Starting ${productName} from "${directoryForExes}"`);
|
||||
logger.info("🐚 Syncing shell environment");
|
||||
await shellSync();
|
||||
|
||||
try {
|
||||
logger.info("🔌 Starting LensProxy");
|
||||
await lensProxy.listen();
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
|
||||
bindBroadcastHandlers();
|
||||
|
||||
return app.exit();
|
||||
}
|
||||
powerMonitor.on("shutdown", () => app.exit());
|
||||
|
||||
// test proxy connection
|
||||
try {
|
||||
logger.info("🔎 Testing LensProxy connection ...");
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
if (getAppVersion() !== versionFromProxy) {
|
||||
logger.error("Proxy server responded with invalid response");
|
||||
PrometheusProviderRegistry.createInstance();
|
||||
initializers.initPrometheusProviderRegistry();
|
||||
|
||||
/**
|
||||
* The following sync MUST be done before HotbarStore creation, because that
|
||||
* store has migrations that will remove items that previous migrations add
|
||||
* if this is not present
|
||||
*/
|
||||
syncGeneralEntities();
|
||||
|
||||
logger.info("💾 Loading stores");
|
||||
|
||||
const userStore = di.inject(userStoreInjectable);
|
||||
|
||||
userStore.startMainReactions();
|
||||
|
||||
// ClusterStore depends on: UserStore
|
||||
const clusterStore = di.inject(clusterStoreInjectable);
|
||||
|
||||
clusterStore.provideInitialFromMain();
|
||||
|
||||
// HotbarStore depends on: ClusterStore
|
||||
HotbarStore.createInstance();
|
||||
|
||||
WeblinkStore.createInstance();
|
||||
|
||||
syncWeblinks();
|
||||
|
||||
HelmRepoManager.createInstance(); // create the instance
|
||||
|
||||
const router = di.inject(routerInjectable);
|
||||
const shellApiRequest = di.inject(shellApiRequestInjectable);
|
||||
|
||||
const lensProxy = LensProxy.createInstance(router, {
|
||||
getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req),
|
||||
kubeApiRequest,
|
||||
shellApiRequest,
|
||||
});
|
||||
|
||||
ClusterManager.createInstance().init();
|
||||
|
||||
initializers.initClusterMetadataDetectors();
|
||||
|
||||
try {
|
||||
logger.info("🔌 Starting LensProxy");
|
||||
await lensProxy.listen();
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
|
||||
|
||||
return app.exit();
|
||||
}
|
||||
|
||||
logger.info("⚡ LensProxy connection OK");
|
||||
} catch (error) {
|
||||
logger.error(`🛑 LensProxy: failed connection test: ${error}`);
|
||||
// test proxy connection
|
||||
try {
|
||||
logger.info("🔎 Testing LensProxy connection ...");
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
|
||||
|
||||
const hostsPath = isWindows
|
||||
? "C:\\windows\\system32\\drivers\\etc\\hosts"
|
||||
: "/etc/hosts";
|
||||
const message = [
|
||||
`Failed connection test: ${error}`,
|
||||
"Check to make sure that no other versions of Lens are running",
|
||||
`Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`,
|
||||
"If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.",
|
||||
];
|
||||
if (getAppVersion() !== versionFromProxy) {
|
||||
logger.error("Proxy server responded with invalid response");
|
||||
|
||||
dialog.showErrorBox("Lens Proxy Error", message.join("\n\n"));
|
||||
return app.exit();
|
||||
}
|
||||
|
||||
return app.exit();
|
||||
}
|
||||
logger.info("⚡ LensProxy connection OK");
|
||||
} catch (error) {
|
||||
logger.error(`🛑 LensProxy: failed connection test: ${error}`);
|
||||
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
const hostsPath = isWindows
|
||||
? "C:\\windows\\system32\\drivers\\etc\\hosts"
|
||||
: "/etc/hosts";
|
||||
const message = [
|
||||
`Failed connection test: ${error}`,
|
||||
"Check to make sure that no other versions of Lens are running",
|
||||
`Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`,
|
||||
"If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.",
|
||||
];
|
||||
|
||||
extensionLoader.init();
|
||||
dialog.showErrorBox("Lens Proxy Error", message.join("\n\n"));
|
||||
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
return app.exit();
|
||||
}
|
||||
|
||||
extensionDiscovery.init();
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
// Start the app without showing the main window when auto starting on login
|
||||
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
|
||||
extensionLoader.init();
|
||||
|
||||
logger.info("🖥️ Starting WindowManager");
|
||||
const windowManager = WindowManager.createInstance();
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
|
||||
onQuitCleanup.push(
|
||||
initMenu(windowManager, menuItems),
|
||||
initTray(windowManager),
|
||||
() => ShellSession.cleanup(),
|
||||
);
|
||||
extensionDiscovery.init();
|
||||
|
||||
installDeveloperTools();
|
||||
// Start the app without showing the main window when auto starting on login
|
||||
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
|
||||
|
||||
if (!startHidden) {
|
||||
windowManager.ensureMainWindow();
|
||||
}
|
||||
logger.info("🖥️ Starting WindowManager");
|
||||
const windowManager = WindowManager.createInstance();
|
||||
|
||||
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
|
||||
onCloseCleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
|
||||
await ensureDir(storedKubeConfigFolder());
|
||||
KubeconfigSyncManager.getInstance().startSync();
|
||||
startUpdateChecking();
|
||||
lensProtocolRouterMain.rendererLoaded = true;
|
||||
const menuItems = di.inject(electronMenuItemsInjectable);
|
||||
|
||||
onQuitCleanup.push(
|
||||
initMenu(windowManager, menuItems),
|
||||
initTray(windowManager),
|
||||
() => ShellSession.cleanup(),
|
||||
);
|
||||
|
||||
installDeveloperTools();
|
||||
|
||||
if (!startHidden) {
|
||||
windowManager.ensureMainWindow();
|
||||
}
|
||||
|
||||
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
|
||||
onCloseCleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
|
||||
|
||||
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
|
||||
|
||||
await ensureDir(directoryForKubeConfigs);
|
||||
|
||||
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
|
||||
|
||||
kubeConfigSyncManager.startSync();
|
||||
|
||||
startUpdateChecking();
|
||||
lensProtocolRouterMain.rendererLoaded = true;
|
||||
});
|
||||
|
||||
logger.info("🧩 Initializing extensions");
|
||||
|
||||
// call after windowManager to see splash earlier
|
||||
try {
|
||||
const extensions = await extensionDiscovery.load();
|
||||
|
||||
// Start watching after bundled extensions are loaded
|
||||
extensionDiscovery.watchExtensions();
|
||||
|
||||
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
||||
extensionDiscovery.events
|
||||
.on("add", (extension: InstalledExtension) => {
|
||||
extensionLoader.addExtension(extension);
|
||||
})
|
||||
.on("remove", (lensExtensionId: LensExtensionId) => {
|
||||
extensionLoader.removeExtension(lensExtensionId);
|
||||
});
|
||||
|
||||
extensionLoader.initExtensions(extensions);
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||
console.error(error);
|
||||
console.trace();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
appEventBus.emit({ name: "service", action: "start" });
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
logger.info("🧩 Initializing extensions");
|
||||
app.on("activate", (event, hasVisibleWindows) => {
|
||||
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
||||
|
||||
// call after windowManager to see splash earlier
|
||||
try {
|
||||
const extensions = await extensionDiscovery.load();
|
||||
if (!hasVisibleWindows) {
|
||||
WindowManager.getInstance(false)?.ensureMainWindow(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Start watching after bundled extensions are loaded
|
||||
extensionDiscovery.watchExtensions();
|
||||
/**
|
||||
* This variable should is used so that `autoUpdater.installAndQuit()` works
|
||||
*/
|
||||
let blockQuit = !isIntegrationTesting;
|
||||
|
||||
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
||||
extensionDiscovery.events
|
||||
.on("add", (extension: InstalledExtension) => {
|
||||
extensionLoader.addExtension(extension);
|
||||
})
|
||||
.on("remove", (lensExtensionId: LensExtensionId) => {
|
||||
extensionLoader.removeExtension(lensExtensionId);
|
||||
});
|
||||
autoUpdater.on("before-quit-for-update", () => {
|
||||
logger.debug("Unblocking quit for update");
|
||||
blockQuit = false;
|
||||
});
|
||||
|
||||
extensionLoader.initExtensions(extensions);
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||
console.error(error);
|
||||
console.trace();
|
||||
}
|
||||
app.on("will-quit", (event) => {
|
||||
logger.debug("will-quit message");
|
||||
|
||||
setTimeout(() => {
|
||||
appEventBus.emit({ name: "service", action: "start" });
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
app.on("activate", (event, hasVisibleWindows) => {
|
||||
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
||||
|
||||
if (!hasVisibleWindows) {
|
||||
WindowManager.getInstance(false)?.ensureMainWindow(false);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This variable should is used so that `autoUpdater.installAndQuit()` works
|
||||
*/
|
||||
let blockQuit = !isIntegrationTesting;
|
||||
|
||||
autoUpdater.on("before-quit-for-update", () => {
|
||||
logger.debug("Unblocking quit for update");
|
||||
blockQuit = false;
|
||||
});
|
||||
|
||||
app.on("will-quit", (event) => {
|
||||
logger.debug("will-quit message");
|
||||
|
||||
// This is called when the close button of the main window is clicked
|
||||
// This is called when the close button of the main window is clicked
|
||||
|
||||
|
||||
logger.info("APP:QUIT");
|
||||
appEventBus.emit({ name: "app", action: "close" });
|
||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
||||
onCloseCleanup();
|
||||
logger.info("APP:QUIT");
|
||||
appEventBus.emit({ name: "app", action: "close" });
|
||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||
|
||||
// This is set to false here so that LPRM can wait to send future lens://
|
||||
// requests until after it loads again
|
||||
lensProtocolRouterMain.rendererLoaded = false;
|
||||
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
|
||||
|
||||
if (blockQuit) {
|
||||
// Quit app on Cmd+Q (MacOS)
|
||||
kubeConfigSyncManager.stopSync();
|
||||
|
||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||
onCloseCleanup();
|
||||
|
||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||
}
|
||||
// This is set to false here so that LPRM can wait to send future lens://
|
||||
// requests until after it loads again
|
||||
lensProtocolRouterMain.rendererLoaded = false;
|
||||
|
||||
lensProtocolRouterMain.cleanup();
|
||||
onQuitCleanup();
|
||||
});
|
||||
if (blockQuit) {
|
||||
// Quit app on Cmd+Q (MacOS)
|
||||
|
||||
app.on("open-url", (event, rawUrl) => {
|
||||
logger.debug("open-url message");
|
||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||
|
||||
// lens:// protocol handler
|
||||
event.preventDefault();
|
||||
lensProtocolRouterMain.route(rawUrl);
|
||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||
}
|
||||
|
||||
lensProtocolRouterMain.cleanup();
|
||||
onQuitCleanup();
|
||||
});
|
||||
|
||||
app.on("open-url", (event, rawUrl) => {
|
||||
logger.debug("open-url message");
|
||||
|
||||
// lens:// protocol handler
|
||||
event.preventDefault();
|
||||
lensProtocolRouterMain.route(rawUrl);
|
||||
});
|
||||
|
||||
logger.debug("[APP-MAIN] waiting for 'ready' and other messages");
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -360,5 +388,3 @@ export {
|
||||
Mobx,
|
||||
LensExtensions,
|
||||
};
|
||||
|
||||
logger.debug("[APP-MAIN] waiting for 'ready' and other messages");
|
||||
@@ -19,5 +19,4 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
export * from "./metrics-providers";
|
||||
export * from "./ipc";
|
||||
export * from "./cluster-metadata-detectors";
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import electronMenuItemsInjectable from "../../menu/electron-menu-items.injectable";
|
||||
import directoryForLensLocalStorageInjectable
|
||||
from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import { initIpcMainHandlers } from "./init-ipc-main-handlers";
|
||||
|
||||
const initIpcMainHandlersInjectable = getInjectable({
|
||||
instantiate: (di) => initIpcMainHandlers({
|
||||
electronMenuItems: di.inject(electronMenuItemsInjectable),
|
||||
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default initIpcMainHandlersInjectable;
|
||||
+21
-17
@@ -20,25 +20,29 @@
|
||||
*/
|
||||
|
||||
import { BrowserWindow, dialog, IpcMainInvokeEvent, Menu } from "electron";
|
||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../common/cluster-ipc";
|
||||
import type { ClusterId } from "../../common/cluster-types";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../common/ipc";
|
||||
import { catalogEntityRegistry } from "../catalog";
|
||||
import { pushCatalogToRenderer } from "../catalog-pusher";
|
||||
import { ClusterManager } from "../cluster-manager";
|
||||
import { ResourceApplier } from "../resource-applier";
|
||||
import { IpcMainWindowEvents, WindowManager } from "../window-manager";
|
||||
import { clusterFrameMap } from "../../../common/cluster-frames";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/cluster-ipc";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { appEventBus } from "../../../common/app-event-bus/event-bus";
|
||||
import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
|
||||
import { catalogEntityRegistry } from "../../catalog";
|
||||
import { pushCatalogToRenderer } from "../../catalog-pusher";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
import { ResourceApplier } from "../../resource-applier";
|
||||
import { IpcMainWindowEvents, WindowManager } from "../../window-manager";
|
||||
import path from "path";
|
||||
import { remove } from "fs-extra";
|
||||
import { AppPaths } from "../../common/app-paths";
|
||||
import { getAppMenu } from "../menu/menu";
|
||||
import type { MenuRegistration } from "../menu/menu-registration";
|
||||
import { getAppMenu } from "../../menu/menu";
|
||||
import type { MenuRegistration } from "../../menu/menu-registration";
|
||||
import type { IComputedValue } from "mobx";
|
||||
|
||||
export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegistration[]>) {
|
||||
interface Dependencies {
|
||||
electronMenuItems: IComputedValue<MenuRegistration[]>,
|
||||
directoryForLensLocalStorage: string;
|
||||
}
|
||||
|
||||
export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalStorage }: Dependencies) => () => {
|
||||
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||
return ClusterStore.getInstance()
|
||||
.getById(clusterId)
|
||||
@@ -94,7 +98,7 @@ export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegist
|
||||
|
||||
try {
|
||||
// remove the local storage file
|
||||
const localStorageFilePath = path.resolve(AppPaths.get("userData"), "lens-local-storage", `${cluster.id}.json`);
|
||||
const localStorageFilePath = path.resolve(directoryForLensLocalStorage, `${cluster.id}.json`);
|
||||
|
||||
await remove(localStorageFilePath);
|
||||
} catch {}
|
||||
@@ -163,4 +167,4 @@ export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegist
|
||||
|
||||
menu.popup(options);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -23,7 +23,7 @@ import request, { RequestPromiseOptions } from "request-promise-native";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
|
||||
import { LensProxy } from "./lens-proxy";
|
||||
import type { Cluster } from "./cluster";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
|
||||
export async function k8sRequest<T = any>(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
const kubeProxyUrl = `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { KubeAuthProxy } from "./kube-auth-proxy";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import bundledKubectlInjectable from "../kubectl/bundled-kubectl.injectable";
|
||||
|
||||
const createKubeAuthProxyInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const bundledKubectl = di.inject(bundledKubectlInjectable);
|
||||
|
||||
const dependencies = {
|
||||
getProxyBinPath: bundledKubectl.getPath,
|
||||
};
|
||||
|
||||
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) =>
|
||||
new KubeAuthProxy(dependencies, cluster, environmentVariables);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createKubeAuthProxyInjectable;
|
||||
@@ -22,15 +22,18 @@
|
||||
import { ChildProcess, spawn } from "child_process";
|
||||
import { waitUntilUsed } from "tcp-port-used";
|
||||
import { randomBytes } from "crypto";
|
||||
import type { Cluster } from "./cluster";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import logger from "./logger";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import logger from "../logger";
|
||||
import * as url from "url";
|
||||
import { getPortFrom } from "./utils/get-port";
|
||||
import { getPortFrom } from "../utils/get-port";
|
||||
import { makeObservable, observable, when } from "mobx";
|
||||
|
||||
const startingServeRegex = /^starting to serve on (?<address>.+)/i;
|
||||
|
||||
interface Dependencies {
|
||||
getProxyBinPath: () => Promise<string>;
|
||||
}
|
||||
|
||||
export class KubeAuthProxy {
|
||||
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
|
||||
|
||||
@@ -43,7 +46,7 @@ export class KubeAuthProxy {
|
||||
protected readonly acceptHosts: string;
|
||||
@observable protected ready = false;
|
||||
|
||||
constructor(protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
|
||||
constructor(private dependencies: Dependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
|
||||
makeObservable(this);
|
||||
|
||||
this.acceptHosts = url.parse(this.cluster.apiUrl).hostname;
|
||||
@@ -58,7 +61,7 @@ export class KubeAuthProxy {
|
||||
return this.whenReady;
|
||||
}
|
||||
|
||||
const proxyBin = await Kubectl.bundled().getPath();
|
||||
const proxyBin = await this.dependencies.getProxyBinPath();
|
||||
const args = [
|
||||
"proxy",
|
||||
"-p", "0",
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||
|
||||
export interface KubeConfigManagerInstantiationParameter {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
const createKubeconfigManagerInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const dependencies = {
|
||||
directoryForTemp: di.inject(directoryForTempInjectable),
|
||||
};
|
||||
|
||||
return (cluster: Cluster) => new KubeconfigManager(dependencies, cluster);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createKubeconfigManagerInjectable;
|
||||
@@ -20,14 +20,17 @@
|
||||
*/
|
||||
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Cluster } from "./cluster";
|
||||
import type { ContextHandler } from "./context-handler";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import { dumpConfigYaml } from "../common/kube-helpers";
|
||||
import logger from "./logger";
|
||||
import { LensProxy } from "./lens-proxy";
|
||||
import { AppPaths } from "../common/app-paths";
|
||||
import { dumpConfigYaml } from "../../common/kube-helpers";
|
||||
import logger from "../logger";
|
||||
import { LensProxy } from "../lens-proxy";
|
||||
|
||||
interface Dependencies {
|
||||
directoryForTemp: string
|
||||
}
|
||||
|
||||
export class KubeconfigManager {
|
||||
/**
|
||||
@@ -39,7 +42,11 @@ export class KubeconfigManager {
|
||||
*/
|
||||
protected tempFilePath: string | null | undefined = null;
|
||||
|
||||
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) { }
|
||||
protected contextHandler: ContextHandler;
|
||||
|
||||
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
|
||||
this.contextHandler = cluster.contextHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -98,7 +105,10 @@ export class KubeconfigManager {
|
||||
protected async createProxyKubeconfig(): Promise<string> {
|
||||
const { cluster } = this;
|
||||
const { contextName, id } = cluster;
|
||||
const tempFile = path.join(AppPaths.get("temp"), `kubeconfig-${id}`);
|
||||
const tempFile = path.join(
|
||||
this.dependencies.directoryForTemp,
|
||||
`kubeconfig-${id}`,
|
||||
);
|
||||
const kubeConfig = await cluster.getKubeconfig();
|
||||
const proxyConfig: Partial<KubeConfig> = {
|
||||
currentContext: contextName,
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getBundledKubectlVersion } from "../../common/utils";
|
||||
import createKubectlInjectable from "./create-kubectl.injectable";
|
||||
|
||||
const bundledKubectlInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createKubectl = di.inject(createKubectlInjectable);
|
||||
|
||||
const bundledKubectlVersion = getBundledKubectlVersion();
|
||||
|
||||
return createKubectl(bundledKubectlVersion);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default bundledKubectlInjectable;
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import directoryForKubectlBinariesInjectable from "./directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable";
|
||||
import userStoreInjectable from "../../common/user-store/user-store.injectable";
|
||||
|
||||
const createKubectlInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const dependencies = {
|
||||
userStore: di.inject(userStoreInjectable),
|
||||
|
||||
directoryForKubectlBinaries: di.inject(
|
||||
directoryForKubectlBinariesInjectable,
|
||||
),
|
||||
};
|
||||
|
||||
return (clusterVersion: string) =>
|
||||
new Kubectl(dependencies, clusterVersion);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createKubectlInjectable;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
|
||||
import path from "path";
|
||||
|
||||
const directoryForKubectlBinariesInjectable = getInjectable({
|
||||
instantiate: (di) =>
|
||||
path.join(di.inject(directoryForBinariesInjectable), "kubectl"),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForKubectlBinariesInjectable;
|
||||
@@ -21,18 +21,16 @@
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { promiseExecFile } from "../common/utils/promise-exec";
|
||||
import logger from "./logger";
|
||||
import { promiseExecFile } from "../../common/utils/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 { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
||||
import { helmCli } from "../helm/helm-cli";
|
||||
import { customRequest } from "../../common/request";
|
||||
import { getBundledKubectlVersion } from "../../common/utils/app-version";
|
||||
import { isDevelopment, isWindows, isTestEnv } from "../../common/vars";
|
||||
import { SemVer } from "semver";
|
||||
import { defaultPackageMirror, packageMirrors } from "../common/user-store/preferences-helpers";
|
||||
import { AppPaths } from "../common/app-paths";
|
||||
import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers";
|
||||
|
||||
const bundledVersion = getBundledKubectlVersion();
|
||||
const kubectlMap: Map<string, string> = new Map([
|
||||
@@ -73,6 +71,17 @@ export function bundledKubectlPath(): string {
|
||||
return bundledPath;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
directoryForKubectlBinaries: string;
|
||||
|
||||
userStore: {
|
||||
kubectlBinariesPath?: string
|
||||
downloadBinariesPath?: string
|
||||
downloadKubectlBinaries: boolean
|
||||
downloadMirror: string
|
||||
};
|
||||
}
|
||||
|
||||
export class Kubectl {
|
||||
public kubectlVersion: string;
|
||||
protected directory: string;
|
||||
@@ -80,20 +89,10 @@ export class Kubectl {
|
||||
protected path: string;
|
||||
protected dirname: string;
|
||||
|
||||
static get kubectlDir() {
|
||||
return path.join(AppPaths.get("userData"), "binaries", "kubectl");
|
||||
}
|
||||
|
||||
public static readonly bundledKubectlVersion: string = bundledVersion;
|
||||
public static invalidBundle = false;
|
||||
private static bundledInstance: Kubectl;
|
||||
|
||||
// Returns the single bundled Kubectl instance
|
||||
public static bundled() {
|
||||
return Kubectl.bundledInstance ??= new Kubectl(Kubectl.bundledKubectlVersion);
|
||||
}
|
||||
|
||||
constructor(clusterVersion: string) {
|
||||
constructor(private dependencies: Dependencies, clusterVersion: string) {
|
||||
let version: SemVer;
|
||||
|
||||
try {
|
||||
@@ -138,23 +137,23 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
public getPathFromPreferences() {
|
||||
return UserStore.getInstance().kubectlBinariesPath || this.getBundledPath();
|
||||
return this.dependencies.userStore.kubectlBinariesPath || this.getBundledPath();
|
||||
}
|
||||
|
||||
protected getDownloadDir() {
|
||||
if (UserStore.getInstance().downloadBinariesPath) {
|
||||
return path.join(UserStore.getInstance().downloadBinariesPath, "kubectl");
|
||||
if (this.dependencies.userStore.downloadBinariesPath) {
|
||||
return path.join(this.dependencies.userStore.downloadBinariesPath, "kubectl");
|
||||
}
|
||||
|
||||
return Kubectl.kubectlDir;
|
||||
return this.dependencies.directoryForKubectlBinaries;
|
||||
}
|
||||
|
||||
public async getPath(bundled = false): Promise<string> {
|
||||
public getPath = async (bundled = false): Promise<string> => {
|
||||
if (bundled) {
|
||||
return this.getBundledPath();
|
||||
}
|
||||
|
||||
if (UserStore.getInstance().downloadKubectlBinaries === false) {
|
||||
if (this.dependencies.userStore.downloadKubectlBinaries === false) {
|
||||
return this.getPathFromPreferences();
|
||||
}
|
||||
|
||||
@@ -179,7 +178,7 @@ export class Kubectl {
|
||||
|
||||
return this.getBundledPath();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public async binDir() {
|
||||
try {
|
||||
@@ -253,7 +252,7 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
public async ensureKubectl(): Promise<boolean> {
|
||||
if (UserStore.getInstance().downloadKubectlBinaries === false) {
|
||||
if (this.dependencies.userStore.downloadKubectlBinaries === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -339,7 +338,7 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
protected async writeInitScripts() {
|
||||
const kubectlPath = UserStore.getInstance().downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
|
||||
const kubectlPath = this.dependencies.userStore.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
|
||||
const helmPath = helmCli.getBinaryDir();
|
||||
const fsPromises = fs.promises;
|
||||
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
||||
@@ -399,7 +398,7 @@ export class Kubectl {
|
||||
protected getDownloadMirror(): string {
|
||||
// MacOS packages are only available from default
|
||||
|
||||
const mirror = packageMirrors.get(UserStore.getInstance().downloadMirror)
|
||||
const mirror = packageMirrors.get(this.dependencies.userStore.downloadMirror)
|
||||
?? packageMirrors.get(defaultPackageMirror);
|
||||
|
||||
return mirror.url;
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import packageInfo from "../../package.json";
|
||||
import path from "path";
|
||||
import { Kubectl } from "../../src/main/kubectl";
|
||||
import { isWindows } from "../common/vars";
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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";
|
||||
|
||||
if (isWindows) {
|
||||
binaryName += ".exe";
|
||||
}
|
||||
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";
|
||||
|
||||
if (isWindows) {
|
||||
binaryName += ".exe";
|
||||
}
|
||||
expect(kubectlPath).toBe(binaryName);
|
||||
});
|
||||
});
|
||||
@@ -25,12 +25,12 @@ import spdy from "spdy";
|
||||
import httpProxy from "http-proxy";
|
||||
import { apiPrefix, apiKubePrefix } from "../common/vars";
|
||||
import type { Router } from "./router";
|
||||
import type { ContextHandler } from "./context-handler";
|
||||
import type { ContextHandler } from "./context-handler/context-handler";
|
||||
import logger from "./logger";
|
||||
import { Singleton } from "../common/utils";
|
||||
import type { Cluster } from "./cluster";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { getBoolean } from "./utils/parse-query";
|
||||
|
||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
|
||||
|
||||
@@ -32,8 +32,8 @@ describe("electron-menu-items", () => {
|
||||
let electronMenuItems: IComputedValue<MenuRegistration[]>;
|
||||
let extensionsStub: ObservableMap<string, LensMainExtension>;
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting();
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
extensionsStub = new ObservableMap();
|
||||
|
||||
@@ -42,6 +42,8 @@ describe("electron-menu-items", () => {
|
||||
() => computed(() => [...extensionsStub.values()]),
|
||||
);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
electronMenuItems = di.inject(electronMenuItemsInjectable);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import { LensExtension } from "../../../extensions/main-api";
|
||||
import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
|
||||
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
||||
import mockFs from "mock-fs";
|
||||
import { AppPaths } from "../../../common/app-paths";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import extensionLoaderInjectable
|
||||
from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||
@@ -39,24 +38,6 @@ import extensionsStoreInjectable
|
||||
|
||||
jest.mock("../../../common/ipc");
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getName: () => "lens",
|
||||
setName: jest.fn(),
|
||||
setPath: jest.fn(),
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
function throwIfDefined(val: any): void {
|
||||
if (val != null) {
|
||||
throw val;
|
||||
@@ -70,16 +51,19 @@ describe("protocol router tests", () => {
|
||||
let lpr: LensProtocolRouterMain;
|
||||
let extensionsStore: ExtensionsStore;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
extensionsStore = di.inject(extensionsStoreInjectable);
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs({
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
extensionsStore = di.inject(extensionsStoreInjectable);
|
||||
|
||||
|
||||
lpr = di.inject(lensProtocolRouterMainInjectable);
|
||||
|
||||
lpr.rendererLoaded = true;
|
||||
|
||||
@@ -18,7 +18,5 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./shell-api-request";
|
||||
export * from "./kube-api-request";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { shellApiRequest } from "./shell-api-request";
|
||||
import createShellSessionInjectable from "../../shell-session/create-shell-session.injectable";
|
||||
import shellRequestAuthenticatorInjectable
|
||||
from "./shell-request-authenticator/shell-request-authenticator.injectable";
|
||||
|
||||
const shellApiRequestInjectable = getInjectable({
|
||||
instantiate: (di) => shellApiRequest({
|
||||
createShellSession: di.inject(createShellSessionInjectable),
|
||||
authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate,
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default shellApiRequestInjectable;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import logger from "../../logger";
|
||||
import WebSocket, { Server as WebSocketServer } from "ws";
|
||||
import type { ProxyApiRequestArgs } from "../types";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
import URLParse from "url-parse";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
|
||||
interface Dependencies {
|
||||
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string) => boolean,
|
||||
|
||||
createShellSession: (args: {
|
||||
webSocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
nodeName?: string;
|
||||
}) => { open: () => Promise<void> };
|
||||
}
|
||||
|
||||
export const shellApiRequest = ({ createShellSession, authenticateRequest }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
|
||||
|
||||
if (!cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
|
||||
socket.write("Invalid shell request");
|
||||
|
||||
return void socket.end();
|
||||
}
|
||||
|
||||
const ws = new WebSocketServer({ noServer: true });
|
||||
|
||||
ws.handleUpgrade(req, socket, head, (webSocket) => {
|
||||
const shell = createShellSession({ webSocket, cluster, tabId, nodeName });
|
||||
|
||||
shell.open()
|
||||
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
|
||||
});
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { ShellRequestAuthenticator } from "./shell-request-authenticator";
|
||||
|
||||
const shellRequestAuthenticatorInjectable = getInjectable({
|
||||
instantiate: () => {
|
||||
const authenticator = new ShellRequestAuthenticator();
|
||||
|
||||
authenticator.init();
|
||||
|
||||
return authenticator;
|
||||
},
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default shellRequestAuthenticatorInjectable;
|
||||
+4
-33
@@ -18,22 +18,15 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import logger from "../logger";
|
||||
import { Server as WebSocketServer } from "ws";
|
||||
import { NodeShellSession, LocalShellSession } from "../shell-session";
|
||||
import type { ProxyApiRequestArgs } from "./types";
|
||||
import { ClusterManager } from "../cluster-manager";
|
||||
import URLParse from "url-parse";
|
||||
import { ExtendedMap, Singleton } from "../../common/utils";
|
||||
import type { ClusterId } from "../../common/cluster-types";
|
||||
import { ipcMainHandle } from "../../common/ipc";
|
||||
import { ExtendedMap } from "../../../../common/utils";
|
||||
import type { ClusterId } from "../../../../common/cluster-types";
|
||||
import { ipcMainHandle } from "../../../../common/ipc";
|
||||
import crypto from "crypto";
|
||||
import { promisify } from "util";
|
||||
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
|
||||
export class ShellRequestAuthenticator extends Singleton {
|
||||
export class ShellRequestAuthenticator {
|
||||
private tokens = new ExtendedMap<ClusterId, Map<string, Uint8Array>>();
|
||||
|
||||
init() {
|
||||
@@ -75,25 +68,3 @@ export class ShellRequestAuthenticator extends Singleton {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function shellApiRequest({ req, socket, head }: ProxyApiRequestArgs): void {
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
const { query: { node, shellToken, id: tabId }} = new URLParse(req.url, true);
|
||||
|
||||
if (!cluster || !ShellRequestAuthenticator.getInstance().authenticate(cluster.id, tabId, shellToken)) {
|
||||
socket.write("Invalid shell request");
|
||||
|
||||
return void socket.end();
|
||||
}
|
||||
|
||||
const ws = new WebSocketServer({ noServer: true });
|
||||
|
||||
ws.handleUpgrade(req, socket, head, (webSocket) => {
|
||||
const shell = node
|
||||
? new NodeShellSession(webSocket, cluster, node, tabId)
|
||||
: new LocalShellSession(webSocket, cluster, tabId);
|
||||
|
||||
shell.open()
|
||||
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${node ? "node" : "local"} shell`, error));
|
||||
});
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import type http from "http";
|
||||
import type net from "net";
|
||||
import type { Cluster } from "../cluster";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
|
||||
export interface ProxyApiRequestArgs {
|
||||
req: http.IncomingMessage,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "./cluster";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import type { KubernetesObject } from "@kubernetes/client-node";
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs-extra";
|
||||
@@ -27,7 +27,7 @@ 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 { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { cloneJsonObject } from "../common/utils";
|
||||
import type { Patch } from "rfc6902";
|
||||
import { promiseExecFile } from "../common/utils/promise-exec";
|
||||
|
||||
+7
-3
@@ -24,7 +24,7 @@ import Subtext from "@hapi/subtext";
|
||||
import type http from "http";
|
||||
import path from "path";
|
||||
import { readFile } from "fs-extra";
|
||||
import type { Cluster } from "./cluster";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
||||
import { HelmApiRoute, KubeconfigRoute, MetricsRoute, PortForwardRoute, ResourceApplierApiRoute, VersionRoute } from "./routes";
|
||||
import logger from "./logger";
|
||||
@@ -76,11 +76,15 @@ function getMimeType(filename: string) {
|
||||
return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
routePortForward: (request: LensApiRequest) => Promise<void>
|
||||
}
|
||||
|
||||
export class Router {
|
||||
protected router = new Call.Router();
|
||||
protected static rootPath = path.resolve(__static);
|
||||
|
||||
public constructor() {
|
||||
public constructor(private dependencies: Dependencies) {
|
||||
this.addRoutes();
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ export class Router {
|
||||
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
|
||||
|
||||
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routePortForward);
|
||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, this.dependencies.routePortForward);
|
||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward);
|
||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forwards` }, PortForwardRoute.routeAllPortForwards);
|
||||
this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { Router } from "../router";
|
||||
import routePortForwardInjectable
|
||||
from "../routes/port-forward/route-port-forward/route-port-forward.injectable";
|
||||
|
||||
const routerInjectable = getInjectable({
|
||||
instantiate: (di) => new Router({
|
||||
routePortForward: di.inject(routePortForwardInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default routerInjectable;
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import type { LensApiRequest } from "../router";
|
||||
import { respondJson } from "../utils/http-responses";
|
||||
import type { Cluster } from "../cluster";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
|
||||
|
||||
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import type { LensApiRequest } from "../router";
|
||||
import { respondJson } from "../utils/http-responses";
|
||||
import type { Cluster } from "../cluster";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../common/cluster-types";
|
||||
import logger from "../logger";
|
||||
import { getMetrics } from "../k8s-request";
|
||||
|
||||
@@ -20,159 +20,11 @@
|
||||
*/
|
||||
|
||||
import type { LensApiRequest } from "../router";
|
||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||
import { Kubectl } from "../kubectl";
|
||||
import * as tcpPortUsed from "tcp-port-used";
|
||||
import logger from "../logger";
|
||||
import { getPortFrom } from "../utils/get-port";
|
||||
import { respondJson } from "../utils/http-responses";
|
||||
|
||||
interface PortForwardArgs {
|
||||
clusterId: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
port: number;
|
||||
forwardPort: number;
|
||||
protocol?: string;
|
||||
}
|
||||
|
||||
const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
|
||||
|
||||
class PortForward {
|
||||
public static portForwards: PortForward[] = [];
|
||||
|
||||
static getPortforward(forward: PortForwardArgs) {
|
||||
return PortForward.portForwards.find((pf) => (
|
||||
pf.clusterId == forward.clusterId &&
|
||||
pf.kind == forward.kind &&
|
||||
pf.name == forward.name &&
|
||||
pf.namespace == forward.namespace &&
|
||||
pf.port == forward.port &&
|
||||
(!forward.protocol || pf.protocol == forward.protocol)
|
||||
));
|
||||
}
|
||||
|
||||
public process: ChildProcessWithoutNullStreams;
|
||||
public clusterId: string;
|
||||
public kind: string;
|
||||
public namespace: string;
|
||||
public name: string;
|
||||
public port: number;
|
||||
public forwardPort: number;
|
||||
public protocol: string;
|
||||
|
||||
constructor(public kubeConfig: string, args: PortForwardArgs) {
|
||||
this.clusterId = args.clusterId;
|
||||
this.kind = args.kind;
|
||||
this.namespace = args.namespace;
|
||||
this.name = args.name;
|
||||
this.port = args.port;
|
||||
this.forwardPort = args.forwardPort;
|
||||
this.protocol = args.protocol ?? "http";
|
||||
}
|
||||
|
||||
public async start() {
|
||||
const kubectlBin = await Kubectl.bundled().getPath(true);
|
||||
const args = [
|
||||
"--kubeconfig", this.kubeConfig,
|
||||
"port-forward",
|
||||
"-n", this.namespace,
|
||||
`${this.kind}/${this.name}`,
|
||||
`${this.forwardPort ?? ""}:${this.port}`,
|
||||
];
|
||||
|
||||
this.process = spawn(kubectlBin, args, {
|
||||
env: process.env,
|
||||
});
|
||||
PortForward.portForwards.push(this);
|
||||
this.process.on("exit", () => {
|
||||
const index = PortForward.portForwards.indexOf(this);
|
||||
|
||||
if (index > -1) {
|
||||
PortForward.portForwards.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
this.process.stderr.on("data", (data) => {
|
||||
logger.debug(`[PORT-FORWARD-ROUTE]: kubectl port-forward process stderr: ${data}`);
|
||||
});
|
||||
|
||||
const internalPort = await getPortFrom(this.process.stdout, {
|
||||
lineRegex: internalPortRegex,
|
||||
});
|
||||
|
||||
try {
|
||||
await tcpPortUsed.waitUntilUsed(internalPort, 500, 15000);
|
||||
|
||||
// make sure this.forwardPort is set to the actual port used (if it was 0 then an available port is found by 'kubectl port-forward')
|
||||
this.forwardPort = internalPort;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.process.kill();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
this.process.kill();
|
||||
}
|
||||
}
|
||||
import { PortForward, PortForwardArgs } from "./port-forward/port-forward";
|
||||
|
||||
export class PortForwardRoute {
|
||||
static async routePortForward(request: LensApiRequest) {
|
||||
const { params, query, response, cluster } = request;
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
const protocol = query.get("protocol");
|
||||
|
||||
try {
|
||||
let portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||
namespace, port, forwardPort, protocol,
|
||||
});
|
||||
|
||||
if (!portForward) {
|
||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
||||
|
||||
const thePort = 0 < forwardPort && forwardPort < 65536
|
||||
? forwardPort
|
||||
: 0;
|
||||
|
||||
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
||||
clusterId: cluster.id,
|
||||
kind: resourceType,
|
||||
namespace,
|
||||
name: resourceName,
|
||||
port,
|
||||
forwardPort: thePort,
|
||||
protocol,
|
||||
});
|
||||
|
||||
const started = await portForward.start();
|
||||
|
||||
if (!started) {
|
||||
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
||||
|
||||
return respondJson(response, {
|
||||
message: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`,
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
||||
respondJson(response, { port: portForward.forwardPort });
|
||||
} catch (error) {
|
||||
logger.error(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName });
|
||||
|
||||
return respondJson(response, {
|
||||
message: `Failed to forward port ${port}`,
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
||||
static async routeCurrentPortForward(request: LensApiRequest) {
|
||||
const { params, query, response, cluster } = request;
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { PortForward, PortForwardArgs } from "./port-forward";
|
||||
import bundledKubectlInjectable from "../../kubectl/bundled-kubectl.injectable";
|
||||
|
||||
const createPortForwardInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const bundledKubectl = di.inject(bundledKubectlInjectable);
|
||||
|
||||
const dependencies = {
|
||||
getKubectlBinPath: bundledKubectl.getPath,
|
||||
};
|
||||
|
||||
return (pathToKubeConfig: string, args: PortForwardArgs) =>
|
||||
new PortForward(dependencies, pathToKubeConfig, args);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createPortForwardInjectable;
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import logger from "../../logger";
|
||||
import { getPortFrom } from "../../utils/get-port";
|
||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||
import * as tcpPortUsed from "tcp-port-used";
|
||||
|
||||
const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
|
||||
|
||||
export interface PortForwardArgs {
|
||||
clusterId: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
port: number;
|
||||
forwardPort: number;
|
||||
protocol?: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
getKubectlBinPath: (bundled: boolean) => Promise<string>
|
||||
}
|
||||
|
||||
export class PortForward {
|
||||
public static portForwards: PortForward[] = [];
|
||||
|
||||
static getPortforward(forward: PortForwardArgs) {
|
||||
return PortForward.portForwards.find((pf) => (
|
||||
pf.clusterId == forward.clusterId &&
|
||||
pf.kind == forward.kind &&
|
||||
pf.name == forward.name &&
|
||||
pf.namespace == forward.namespace &&
|
||||
pf.port == forward.port &&
|
||||
(!forward.protocol || pf.protocol == forward.protocol)
|
||||
));
|
||||
}
|
||||
|
||||
public process: ChildProcessWithoutNullStreams;
|
||||
public clusterId: string;
|
||||
public kind: string;
|
||||
public namespace: string;
|
||||
public name: string;
|
||||
public port: number;
|
||||
public forwardPort: number;
|
||||
public protocol: string;
|
||||
|
||||
constructor(private dependencies: Dependencies, public pathToKubeConfig: string, args: PortForwardArgs) {
|
||||
this.clusterId = args.clusterId;
|
||||
this.kind = args.kind;
|
||||
this.namespace = args.namespace;
|
||||
this.name = args.name;
|
||||
this.port = args.port;
|
||||
this.forwardPort = args.forwardPort;
|
||||
this.protocol = args.protocol ?? "http";
|
||||
}
|
||||
|
||||
public async start() {
|
||||
const kubectlBin = await this.dependencies.getKubectlBinPath(true);
|
||||
const args = [
|
||||
"--kubeconfig", this.pathToKubeConfig,
|
||||
"port-forward",
|
||||
"-n", this.namespace,
|
||||
`${this.kind}/${this.name}`,
|
||||
`${this.forwardPort ?? ""}:${this.port}`,
|
||||
];
|
||||
|
||||
this.process = spawn(kubectlBin, args, {
|
||||
env: process.env,
|
||||
});
|
||||
PortForward.portForwards.push(this);
|
||||
this.process.on("exit", () => {
|
||||
const index = PortForward.portForwards.indexOf(this);
|
||||
|
||||
if (index > -1) {
|
||||
PortForward.portForwards.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
this.process.stderr.on("data", (data) => {
|
||||
logger.debug(`[PORT-FORWARD-ROUTE]: kubectl port-forward process stderr: ${data}`);
|
||||
});
|
||||
|
||||
const internalPort = await getPortFrom(this.process.stdout, {
|
||||
lineRegex: internalPortRegex,
|
||||
});
|
||||
|
||||
try {
|
||||
await tcpPortUsed.waitUntilUsed(internalPort, 500, 15000);
|
||||
|
||||
// make sure this.forwardPort is set to the actual port used (if it was 0 then an available port is found by 'kubectl port-forward')
|
||||
this.forwardPort = internalPort;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.process.kill();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
this.process.kill();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { routePortForward } from "./route-port-forward";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import createPortForwardInjectable from "../create-port-forward.injectable";
|
||||
|
||||
const routePortForwardInjectable = getInjectable({
|
||||
instantiate: (di) => routePortForward({
|
||||
createPortForward: di.inject(createPortForwardInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default routePortForwardInjectable;
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { LensApiRequest } from "../../../router";
|
||||
import logger from "../../../logger";
|
||||
import { respondJson } from "../../../utils/http-responses";
|
||||
import { PortForward, PortForwardArgs } from "../port-forward";
|
||||
|
||||
interface Dependencies {
|
||||
createPortForward: (pathToKubeConfig: string, args: PortForwardArgs) => PortForward;
|
||||
}
|
||||
|
||||
export const routePortForward =
|
||||
({ createPortForward }: Dependencies) =>
|
||||
async (request: LensApiRequest) => {
|
||||
const { params, query, response, cluster } = request;
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
const protocol = query.get("protocol");
|
||||
|
||||
try {
|
||||
let portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id,
|
||||
kind: resourceType,
|
||||
name: resourceName,
|
||||
namespace,
|
||||
port,
|
||||
forwardPort,
|
||||
protocol,
|
||||
});
|
||||
|
||||
if (!portForward) {
|
||||
logger.info(
|
||||
`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`,
|
||||
);
|
||||
|
||||
const thePort =
|
||||
0 < forwardPort && forwardPort < 65536 ? forwardPort : 0;
|
||||
|
||||
portForward = createPortForward(await cluster.getProxyKubeconfigPath(), {
|
||||
clusterId: cluster.id,
|
||||
kind: resourceType,
|
||||
namespace,
|
||||
name: resourceName,
|
||||
port,
|
||||
forwardPort: thePort,
|
||||
protocol,
|
||||
});
|
||||
|
||||
const started = await portForward.start();
|
||||
|
||||
if (!started) {
|
||||
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", {
|
||||
namespace,
|
||||
port,
|
||||
resourceType,
|
||||
resourceName,
|
||||
});
|
||||
|
||||
return respondJson(
|
||||
response,
|
||||
{
|
||||
message: `Failed to forward port ${port} to ${
|
||||
thePort ? forwardPort : "random port"
|
||||
}`,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
respondJson(response, { port: portForward.forwardPort });
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`,
|
||||
{ namespace, port, resourceType, resourceName },
|
||||
);
|
||||
|
||||
return respondJson(
|
||||
response,
|
||||
{
|
||||
message: `Failed to forward port ${port}`,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type WebSocket from "ws";
|
||||
import localShellSessionInjectable from "./local-shell-session/local-shell-session.injectable";
|
||||
import nodeShellSessionInjectable from "./node-shell-session/node-shell-session.injectable";
|
||||
|
||||
interface Args {
|
||||
webSocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
nodeName?: string;
|
||||
}
|
||||
|
||||
const createShellSessionInjectable = getInjectable({
|
||||
instantiate:
|
||||
(di) =>
|
||||
({ nodeName, ...rest }: Args) =>
|
||||
!nodeName
|
||||
? di.inject(localShellSessionInjectable, rest)
|
||||
: di.inject(nodeShellSessionInjectable, { nodeName, ...rest }),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createShellSessionInjectable;
|
||||
@@ -19,5 +19,5 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./node-shell-session";
|
||||
export * from "./local-shell-session";
|
||||
export * from "./node-shell-session/node-shell-session";
|
||||
export * from "./local-shell-session/local-shell-session";
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { LocalShellSession } from "./local-shell-session";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import type WebSocket from "ws";
|
||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
||||
|
||||
interface InstantiationParameter {
|
||||
webSocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
const localShellSessionInjectable = getInjectable({
|
||||
instantiate: (di, { cluster, tabId, webSocket }: InstantiationParameter) => {
|
||||
const createKubectl = di.inject(createKubectlInjectable);
|
||||
|
||||
const kubectl = createKubectl(cluster.version);
|
||||
|
||||
return new LocalShellSession(kubectl, webSocket, cluster, tabId);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.transient,
|
||||
});
|
||||
|
||||
export default localShellSessionInjectable;
|
||||
+3
-3
@@ -20,9 +20,9 @@
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import { helmCli } from "../helm/helm-cli";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { ShellSession } from "./shell-session";
|
||||
import { helmCli } from "../../helm/helm-cli";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { ShellSession } from "../shell-session";
|
||||
|
||||
export class LocalShellSession extends ShellSession {
|
||||
ShellType = "shell";
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import type WebSocket from "ws";
|
||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
||||
import { NodeShellSession } from "./node-shell-session";
|
||||
|
||||
interface InstantiationParameter {
|
||||
webSocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
nodeName: string;
|
||||
}
|
||||
|
||||
const nodeShellSessionInjectable = getInjectable({
|
||||
instantiate: (di, { cluster, tabId, webSocket, nodeName }: InstantiationParameter) => {
|
||||
const createKubectl = di.inject(createKubectlInjectable);
|
||||
|
||||
const kubectl = createKubectl(cluster.version);
|
||||
|
||||
return new NodeShellSession(nodeName, kubectl, webSocket, cluster, tabId);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.transient,
|
||||
});
|
||||
|
||||
export default nodeShellSessionInjectable;
|
||||
+9
-8
@@ -23,13 +23,14 @@ import type WebSocket from "ws";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Cluster } from "../cluster";
|
||||
import { ShellOpenError, ShellSession } from "./shell-session";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { ShellOpenError, ShellSession } from "../shell-session";
|
||||
import { get } from "lodash";
|
||||
import { Node, NodesApi } from "../../common/k8s-api/endpoints";
|
||||
import { KubeJsonApi } from "../../common/k8s-api/kube-json-api";
|
||||
import logger from "../logger";
|
||||
import { TerminalChannels } from "../../renderer/api/terminal-api";
|
||||
import { Node, NodesApi } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
|
||||
import logger from "../../logger";
|
||||
import { TerminalChannels } from "../../../renderer/api/terminal-api";
|
||||
import type { Kubectl } from "../../kubectl/kubectl";
|
||||
|
||||
export class NodeShellSession extends ShellSession {
|
||||
ShellType = "node-shell";
|
||||
@@ -39,8 +40,8 @@ export class NodeShellSession extends ShellSession {
|
||||
|
||||
protected readonly cwd: string | undefined = undefined;
|
||||
|
||||
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string, terminalId: string) {
|
||||
super(socket, cluster, terminalId);
|
||||
constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) {
|
||||
super(kubectl, socket, cluster, terminalId);
|
||||
}
|
||||
|
||||
public async open() {
|
||||
@@ -19,8 +19,8 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../cluster";
|
||||
import { Kubectl } from "../kubectl";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { Kubectl } from "../kubectl/kubectl";
|
||||
import type WebSocket from "ws";
|
||||
import { shellEnv } from "../utils/shell-env";
|
||||
import { app } from "electron";
|
||||
@@ -30,7 +30,7 @@ import os from "os";
|
||||
import { isMac, isWindows } from "../../common/vars";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import * as pty from "node-pty";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { appEventBus } from "../../common/app-event-bus/event-bus";
|
||||
import logger from "../logger";
|
||||
import { TerminalChannels, TerminalMessage } from "../../renderer/api/terminal-api";
|
||||
import { deserialize, serialize } from "v8";
|
||||
@@ -140,7 +140,6 @@ export abstract class ShellSession {
|
||||
this.processes.clear();
|
||||
}
|
||||
|
||||
protected kubectl: Kubectl;
|
||||
protected running = false;
|
||||
protected kubectlBinDirP: Promise<string>;
|
||||
protected kubeconfigPathP: Promise<string>;
|
||||
@@ -170,8 +169,7 @@ export abstract class ShellSession {
|
||||
return { shellProcess, resume };
|
||||
}
|
||||
|
||||
constructor(protected websocket: WebSocket, protected cluster: Cluster, terminalId: string) {
|
||||
this.kubectl = new Kubectl(cluster.version);
|
||||
constructor(protected kubectl: Kubectl, protected websocket: WebSocket, protected cluster: Cluster, terminalId: string) {
|
||||
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
||||
this.kubectlBinDirP = this.kubectl.binDir();
|
||||
this.terminalId = `${cluster.id}:${terminalId}`;
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { ClusterId } from "../common/cluster-types";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
|
||||
import windowStateKeeper from "electron-window-state";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { BundledExtensionsLoaded, ipcMainOn } from "../common/ipc";
|
||||
import { delay, iter, Singleton } from "../common/utils";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||
|
||||
Reference in new issue
Block a user