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:
Janne SavolainenandMikko Aspiala committed 2022-01-05 13:26:49 +02:00
1 parent 908a40975e
commit 9d33fff906
336 files changed
+9105 -3307

No files matched your search

+30 -29
View File
@@ -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();
});
});
+42 -20
View File
@@ -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");
});
+34 -25
View File
@@ -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 () => {
+26 -29
View File
@@ -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);
-3
View File
@@ -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);
};
@@ -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;
+30
View File
@@ -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();
});
-1
View File
@@ -20,5 +20,4 @@
*/
export { syncWeblinks } from "./weblinks";
export { KubeconfigSyncManager } from "./kubeconfig-sync";
export { syncGeneralEntities } from "./general";
@@ -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;
@@ -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 {
+2 -2
View File
@@ -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]:";
-724
View File
@@ -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;
+1 -1
View File
@@ -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";
-81
View File
@@ -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),
});
}
}
+4
View File
@@ -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)$/);
+36 -4
View File
@@ -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 }),
]);
+1 -1
View File
@@ -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
View File
@@ -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");
-1
View File
@@ -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;
@@ -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);
});
}
};
+1 -1
View File
@@ -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;
@@ -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;
-72
View File
@@ -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);
});
});
+3 -3
View File
@@ -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;
+4 -2
View File
@@ -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;
-2
View File
@@ -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));
});
};
@@ -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;
@@ -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));
});
}
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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);
+34
View File
@@ -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;
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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";
+1 -149
View File
@@ -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;
+2 -2
View File
@@ -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;
@@ -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;
@@ -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() {
+4 -6
View File
@@ -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}`;
+1 -1
View File
@@ -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";