Making apiBase injectable (#6022)

* Making apiBase injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all of Helm functions to be DI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make PortForward's use of apiBase fully injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all metric requests to be injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace resource applier with injectables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch KubeJsonApi.forCluster to be injectable but do not use

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert the rest of shell sessions to be DI-ed

- This is a prerequesit for using the new
  createKubeJsonApiForClusterInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Use new createKubeJsonApiForClusterInjectable for openNodeShellSession

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make KubeconfigDialog injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove jest-fetch-mock and make fetch injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests with new global override

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add new injectable for create KubeJsonApi and JsonApi instances

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix showing-details-for-helm-release behavioural tests

- Remove HelmChartStore in favour of all injectables

- Create a model for UpgradeChartDockTab

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix show details and updating helm releases tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix residual typing issues related to metrics

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crash on load due to circular dependency

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix create resource tab not working

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove legacy apiBase global

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce and use isDebuggingInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce and use windowLocationInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove global legacy apiKube

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve injectable filenames compared to the injectables inside

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove modifying input in requestActivePortForwardInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce and use get(Milli)SecondsFromUnixEpochInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch to non-reactive way of gettting possible helm release versions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix typo

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix bug in KubeApi constructor

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all KubeApi related tests to use asyncFn

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests after introducing new injectables that have side effects

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix bad rebase causing tests to fail

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve expects for multiple field values

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crash will looking up api refs

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix breaking change on KubeApi.list

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Better fix for formatting urls

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove injectable for time since we should just use useMockTime

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add happy path behavioural tests for upgrade chart tab

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove debug message

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix showing-details-for-helm-release tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix installing-helm-chart-from-new-tab tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests relating to hosted cluster id

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots to recent changes in master

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Reupdated upgrade chart new tab test snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix flakiness in unit test when using <Animated>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix flakiness and improve tests for DeleteClusterDialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix kubeconfig-sync tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix <Extensions> tests by removing mockFs and making everything injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix build issues

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix getElectronAppPathInjectable override not returning absolute paths

- Also fixes the listing-active-helm-repos-in-prefs tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace all uses of getAbsolutePath with joinPaths as it is more correct and less confusing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix opening application window tests by making override properly absolute

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots relating no longer using getAbsolutePath

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix and add behavioural tests for RenderDelay

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension discovery tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test flakiness because of path side effects, propagate uses to as many places

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension-discovery tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override to fix some tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Rewrite and fix implementation of KubeconfigManager and its tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by global override pathExists

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests failing on windows by using injectable verions of path functions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Attempt to fix test timeout by using runInAction

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix setupIpcMainHandlers usage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
authored and GitHub committed 2022-10-05 08:10:36 -04:00
1 parent ed073d6562
commit 76066c5ebf
367 files changed
+16701 -5302

No files matched your search

+231 -62
View File
@@ -4,12 +4,7 @@
*/
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import mockFs from "mock-fs";
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 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";
@@ -18,31 +13,53 @@ import type { DiContainer } from "@ogre-tools/injectable";
import { parse } from "url";
import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger";
import assert from "assert";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS
import type { ReadFile } from "../../common/fs/read-file.injectable";
import readFileInjectable from "../../common/fs/read-file.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { WriteFile } from "../../common/fs/write-file.injectable";
import writeFileInjectable from "../../common/fs/write-file.injectable";
import type { PathExists } from "../../common/fs/path-exists.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import type { DeleteFile } from "../../common/fs/delete-file.injectable";
import deleteFileInjectable from "../../common/fs/delete-file.injectable";
const clusterServerUrl = "https://192.168.64.3:8443";
describe("kubeconfig manager tests", () => {
let clusterFake: Cluster;
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined;
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
let di: DiContainer;
let loggerMock: jest.Mocked<Logger>;
let readFileMock: AsyncFnMock<ReadFile>;
let deleteFileMock: AsyncFnMock<DeleteFile>;
let writeFileMock: AsyncFnMock<WriteFile>;
let pathExistsMock: AsyncFnMock<PathExists>;
let kubeConfManager: KubeconfigManager;
let ensureServerMock: AsyncFnMock<() => Promise<void>>;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "/some-directory-for-temp");
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin");
readFileMock = asyncFn();
di.override(readFileInjectable, () => readFileMock);
writeFileMock = asyncFn();
di.override(writeFileInjectable, () => writeFileMock);
pathExistsMock = asyncFn();
di.override(pathExistsInjectable, () => pathExistsMock);
deleteFileMock = asyncFn();
di.override(deleteFileInjectable, () => deleteFileMock);
loggerMock = {
warn: jest.fn(),
debug: jest.fn(),
@@ -53,29 +70,7 @@ describe("kubeconfig manager tests", () => {
di.override(loggerInjectable, () => loggerMock);
mockFs({
"minikube-config.yml": JSON.stringify({
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: clusterServerUrl,
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
preferences: {},
}),
});
ensureServerMock = asyncFn();
di.override(createContextHandlerInjectable, () => (cluster) => ({
restartServer: jest.fn(),
@@ -86,7 +81,7 @@ describe("kubeconfig manager tests", () => {
resolveAuthProxyCa: jest.fn(),
resolveAuthProxyUrl: jest.fn(),
setupPrometheus: jest.fn(),
ensureServer: jest.fn(),
ensureServer: ensureServerMock,
}));
const createCluster = di.inject(createClusterInjectionToken);
@@ -96,48 +91,222 @@ describe("kubeconfig manager tests", () => {
clusterFake = createCluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
kubeConfigPath: "/minikube-config.yml",
}, {
clusterServerUrl,
});
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
kubeConfManager = createKubeconfigManager(clusterFake);
});
afterEach(() => {
mockFs.restore();
describe("when calling clear", () => {
it("should resolve immediately", async () => {
await kubeConfManager.clear();
});
it("being called several times shouldn't throw", async () => {
await kubeConfManager.clear();
await kubeConfManager.clear();
await kubeConfManager.clear();
});
});
it("should create 'temp' kube config with proxy", async () => {
const kubeConfManager = createKubeconfigManager(clusterFake);
describe("when getPath() is called initially", () => {
let getPathPromise: Promise<string>;
assert(kubeConfManager, "should actually create one");
beforeEach(async () => {
getPathPromise = kubeConfManager.getPath();
});
expect(loggerMock.error).not.toBeCalled();
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());
const yml = loadYaml<any>(file.toString());
it("should not call pathExists()", () => {
expect(pathExistsMock).not.toBeCalled();
});
expect(yml["current-context"]).toBe("minikube");
expect(yml["clusters"][0]["cluster"]["server"].endsWith("/foo")).toBe(true);
expect(yml["users"][0]["name"]).toBe("proxy");
});
it("should call ensureServer on the cluster context", () => {
expect(ensureServerMock).toBeCalledTimes(1);
});
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const kubeConfManager = createKubeconfigManager(clusterFake);
describe("when ensureServer resolves", () => {
beforeEach(async () => {
await ensureServerMock.resolve();
assert(kubeConfManager, "should actually create one");
// clear state of calls
ensureServerMock.mock.calls.length = 0;
});
const configPath = await kubeConfManager.getPath();
describe("when reading cluster's kubeconfig resolves", () => {
beforeEach(async () => {
await readFileMock.resolveSpecific(
["/minikube-config.yml"],
JSON.stringify({
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: clusterServerUrl,
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
preferences: {},
}),
);
});
expect(await fse.pathExists(configPath)).toBe(true);
await kubeConfManager.clear();
expect(await fse.pathExists(configPath)).toBe(false);
await kubeConfManager.clear(); // doesn't throw
expect(async () => {
await kubeConfManager.getPath();
}).rejects.toThrow("already unlinked");
describe("when writing out new proxy kubeconfig resolves", () => {
beforeEach(async () => {
await writeFileMock.resolveSpecific(
["/some-directory-for-temp/kubeconfig-foo", "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n server: http://127.0.0.1:9191/foo\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user: {}\n"],
);
});
it("should allow getPath to resolve with the path to the kubeconfig", async () => {
expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo");
});
describe("when calling clear", () => {
let clearPromise: Promise<void>;
beforeEach(() => {
clearPromise = kubeConfManager.clear();
});
it("should call deleteFile", () => {
expect(deleteFileMock).toBeCalledTimes(1);
});
describe("when deleteFile resolves", () => {
beforeEach(async () => {
await deleteFileMock.resolveSpecific(
["/some-directory-for-temp/kubeconfig-foo"],
);
});
it("should allow clear to resolve", async () => {
await clearPromise;
});
});
describe("when deleteFile rejects with ENOENT", () => {
beforeEach(async () => {
await deleteFileMock.resolveSpecific(
["/some-directory-for-temp/kubeconfig-foo"],
Promise.reject(Object.assign(new Error("file not found"), {
code: "ENOENT",
})),
);
});
it("should allow clear to resolve", async () => {
await clearPromise;
});
});
it("when deleteFile rejects with some other error; clear should also reject", async () => {
const expectPromise = expect(clearPromise).rejects.toBeDefined();
await deleteFileMock.reject(new Error("some other error"));
await expectPromise;
});
});
describe("when calling getPath a second time", () => {
let getPathPromise: Promise<string>;
beforeEach(async () => {
getPathPromise = kubeConfManager.getPath();
});
it("should call pathExists", () => {
expect(pathExistsMock).toBeCalledTimes(1);
});
describe("when pathExists resoves to true", () => {
beforeEach(async () => {
await pathExistsMock.resolveSpecific(
["/some-directory-for-temp/kubeconfig-foo"],
true,
);
});
it("always getPath to resolve with path", async () => {
expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo");
});
});
describe("when pathExists resoves to false", () => {
beforeEach(async () => {
await pathExistsMock.resolveSpecific(
["/some-directory-for-temp/kubeconfig-foo"],
false,
);
});
it("should call ensureServer on the cluster context", () => {
expect(ensureServerMock).toBeCalledTimes(1);
});
describe("when ensureServer resolves", () => {
beforeEach(async () => {
await ensureServerMock.resolve();
});
describe("when reading cluster's kubeconfig resolves", () => {
beforeEach(async () => {
await readFileMock.resolveSpecific(
["/minikube-config.yml"],
JSON.stringify({
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: clusterServerUrl,
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
preferences: {},
}),
);
});
describe("when writing out new proxy kubeconfig resolves", () => {
beforeEach(async () => {
await writeFileMock.resolveSpecific(
["/some-directory-for-temp/kubeconfig-foo", "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n server: http://127.0.0.1:9191/foo\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user: {}\n"],
);
});
it("should allow getPath to resolve with the path to the kubeconfig", async () => {
expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo");
});
});
});
});
});
});
});
});
});
});
});
@@ -6,8 +6,6 @@ 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";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { joinPathsFake } from "../../../common/test-utils/join-paths-fake";
describe("get-electron-app-path", () => {
let getElectronAppPath: (name: string) => string;
@@ -31,7 +29,6 @@ describe("get-electron-app-path", () => {
} as App;
di.override(electronAppInjectable, () => appStub);
di.override(joinPathsInjectable, () => joinPathsFake);
getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string;
});
@@ -16,6 +16,7 @@ import loggerInjectable from "../../common/logger.injectable";
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable";
const createClusterInjectable = getInjectable({
id: "create-cluster",
@@ -32,6 +33,7 @@ const createClusterInjectable = getInjectable({
detectorRegistry: di.inject(detectorRegistryInjectable),
createVersionDetector: di.inject(createVersionDetectorInjectable),
broadcastMessage: di.inject(broadcastMessageInjectable),
loadConfigfromFile: di.inject(loadConfigfromFileInjectable),
};
return (model, configData) => new Cluster(dependencies, model, configData);
@@ -3,37 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import { setupIpcMainHandlers } from "./setup-ipc-main-handlers";
import loggerInjectable from "../../../../common/logger.injectable";
import clusterManagerInjectable from "../../../cluster/manager.injectable";
import applicationMenuItemsInjectable from "../../../menu/application-menu-items.injectable";
import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import operatingSystemThemeInjectable from "../../../theme/operating-system-theme.injectable";
import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable";
import askUserForFilePathsInjectable from "../../../ipc/ask-user-for-file-paths.injectable";
import clustersThatAreBeingDeletedInjectable from "../../../cluster/are-being-deleted.injectable";
const setupIpcMainHandlersInjectable = getInjectable({
id: "setup-ipc-main-handlers",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const directoryForLensLocalStorage = di.inject(
directoryForLensLocalStorageInjectable,
);
const clusterManager = di.inject(clusterManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const clusterStore = di.inject(clusterStoreInjectable);
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
const askUserForFilePaths = di.inject(askUserForFilePathsInjectable);
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return {
id: "setup-ipc-main-handlers",
@@ -42,14 +32,11 @@ const setupIpcMainHandlersInjectable = getInjectable({
setupIpcMainHandlers({
applicationMenuItems,
getAbsolutePath,
directoryForLensLocalStorage,
clusterManager,
catalogEntityRegistry,
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
clustersThatAreBeingDeleted,
});
},
};
@@ -5,7 +5,7 @@
import type { IpcMainInvokeEvent } from "electron";
import { BrowserWindow, Menu } from "electron";
import { clusterFrameMap } from "../../../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../../common/ipc/cluster";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../../../common/ipc/cluster";
import type { ClusterId } from "../../../../common/cluster-types";
import { ClusterStore } from "../../../../common/cluster-store/cluster-store";
import { appEventBus } from "../../../../common/app-event-bus/event-bus";
@@ -14,9 +14,7 @@ import type { CatalogEntityRegistry } from "../../../catalog";
import { pushCatalogToRenderer } from "../../../catalog-pusher";
import type { ClusterManager } from "../../../cluster/manager";
import { ResourceApplier } from "../../../resource-applier";
import { remove } from "fs-extra";
import type { IComputedValue, ObservableSet } from "mobx";
import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable";
import type { IComputedValue } from "mobx";
import type { MenuItemOpts } from "../../../menu/application-menu-items.injectable";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../../common/ipc/window";
import { handleWindowAction, onLocationChange } from "../../../ipc/window";
@@ -26,27 +24,21 @@ import type { Theme } from "../../../theme/operating-system-theme-state.injectab
import type { AskUserForFilePaths } from "../../../ipc/ask-user-for-file-paths.injectable";
interface Dependencies {
directoryForLensLocalStorage: string;
getAbsolutePath: GetAbsolutePath;
applicationMenuItems: IComputedValue<MenuItemOpts[]>;
clusterManager: ClusterManager;
catalogEntityRegistry: CatalogEntityRegistry;
clusterStore: ClusterStore;
operatingSystemTheme: IComputedValue<Theme>;
askUserForFilePaths: AskUserForFilePaths;
clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
}
export const setupIpcMainHandlers = ({
applicationMenuItems,
directoryForLensLocalStorage,
getAbsolutePath,
clusterManager,
catalogEntityRegistry,
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
clustersThatAreBeingDeleted,
}: Dependencies) => {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
@@ -85,40 +77,6 @@ export const setupIpcMainHandlers = ({
}
});
ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "remove" });
const clusterStore = ClusterStore.getInstance();
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
return;
}
cluster.disconnect();
clusterFrameMap.delete(cluster.id);
// Remove from the cluster store as well, this should clear any old settings
clusterStore.clusters.delete(cluster.id);
try {
// remove the local storage file
const localStorageFilePath = getAbsolutePath(directoryForLensLocalStorage, `${cluster.id}.json`);
await remove(localStorageFilePath);
} catch {
// ignore error
}
});
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
clustersThatAreBeingDeleted.add(clusterId);
});
ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => {
clustersThatAreBeingDeleted.delete(clusterId);
});
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
+2 -2
View File
@@ -4,10 +4,10 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../common/cluster/cluster";
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
import type { RequestMetricsParams } from "../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
import k8sRequestInjectable from "./k8s-request.injectable";
export type GetMetrics = (cluster: Cluster, prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) => Promise<any>;
export type GetMetrics = (cluster: Cluster, prometheusPath: string, queryParams: RequestMetricsParams & { query: string }) => Promise<any>;
const getMetricsInjectable = getInjectable({
id: "get-metrics",
+3 -9
View File
@@ -18,10 +18,6 @@ import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader
import type { FileSystemProvisionerStore } from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import type { UserStore } from "../common/user-store";
import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectable";
import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake";
import joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbars/store.injectable";
import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable";
import { EventEmitter } from "../common/event-emitter";
@@ -74,7 +70,7 @@ import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
import periodicalCheckForUpdatesInjectable from "./application-update/periodical-check-for-updates/periodical-check-for-updates.injectable";
import execFileInjectable from "../common/fs/exec-file.injectable";
import normalizedPlatformArchitectureInjectable from "../common/vars/normalized-platform-architecture.injectable";
import getHelmChartInjectable from "./helm/helm-service/get-helm-chart.injectable";
import getHelmChartVersionsInjectable from "./helm/helm-service/get-helm-chart-versions.injectable";
import getHelmChartValuesInjectable from "./helm/helm-service/get-helm-chart-values.injectable";
import listHelmChartsInjectable from "./helm/helm-service/list-helm-charts.injectable";
import deleteHelmReleaseInjectable from "./helm/helm-service/delete-helm-release.injectable";
@@ -161,7 +157,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.override(periodicalCheckForUpdatesInjectable, () => ({ start: () => {}, stop: () => {}, started: false }));
overrideFunctionalInjectables(di, [
getHelmChartInjectable,
getHelmChartVersionsInjectable,
getHelmChartValuesInjectable,
listHelmChartsInjectable,
deleteHelmReleaseInjectable,
@@ -228,8 +224,6 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
const overrideOperatingSystem = (di: DiContainer) => {
di.override(platformInjectable, () => "darwin");
di.override(getAbsolutePathInjectable, () => getAbsolutePathFake);
di.override(joinPathsInjectable, () => joinPathsFake);
di.override(normalizedPlatformArchitectureInjectable, () => "arm64");
};
@@ -267,7 +261,7 @@ const overrideElectronFeatures = (di: DiContainer) => {
di.override(
getElectronAppPathInjectable,
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
() => (name: string) => `/some-electron-app-path-for-${kebabCase(name)}`,
);
di.override(setElectronAppPathInjectable, () => () => {});
+7 -7
View File
@@ -8,7 +8,7 @@ import listHelmChartsInjectable from "../helm-service/list-helm-charts.injectabl
import getActiveHelmRepositoriesInjectable from "../repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { AsyncResult } from "../../../common/utils/async-result";
import type { HelmRepo } from "../../../common/helm/helm-repo";
import { sortCharts } from "../../../common/utils";
import { sortBySemverVersion } from "../../../common/utils";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
describe("Helm Service tests", () => {
@@ -203,7 +203,7 @@ describe("Helm Service tests", () => {
const charts = new Map([
["stable", {
"invalid-semver": sortCharts([
"invalid-semver": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "weird-versioning",
@@ -237,7 +237,7 @@ const charts = new Map([
created: "now",
},
]),
"apm-server": sortCharts([
"apm-server": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "apm-server",
@@ -255,7 +255,7 @@ const charts = new Map([
created: "now",
},
]),
"redis": sortCharts([
"redis": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "apm-server",
@@ -275,7 +275,7 @@ const charts = new Map([
]),
}],
["experiment", {
"fairwind": sortCharts([
"fairwind": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "fairwind",
@@ -296,7 +296,7 @@ const charts = new Map([
]),
}],
["bitnami", {
"hotdog": sortCharts([
"hotdog": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "hotdog",
@@ -314,7 +314,7 @@ const charts = new Map([
created: "now",
},
]),
"pretzel": sortCharts([
"pretzel": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "pretzel",
+3 -3
View File
@@ -4,21 +4,21 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getBinaryName } from "../../common/vars";
import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable";
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
const helmBinaryPathInjectable = getInjectable({
id: "helm-binary-path",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const normalizedPlatform = di.inject(normalizedPlatformInjectable);
const baseBundledBinariesDirectory = di.inject(baseBundledBinariesDirectoryInjectable);
const helmBinaryName = getBinaryName("helm", { forPlatform: normalizedPlatform });
return getAbsolutePath(baseBundledBinariesDirectory, helmBinaryName);
return joinPaths(baseBundledBinariesDirectory, helmBinaryName);
},
});
+3 -3
View File
@@ -5,14 +5,14 @@
import fs from "fs";
import * as yaml from "js-yaml";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
import { iter, put, sortCharts } from "../../common/utils";
import { iter, put, sortBySemverVersion } from "../../common/utils";
import { execHelm } from "./exec";
import type { SetRequired } from "type-fest";
import { assert } from "console";
import type { HelmRepo } from "../../common/helm/helm-repo";
import type { HelmChartManagerCache } from "./helm-chart-manager-cache.injectable";
import type { Logger } from "../../common/logger";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
export interface HelmCacheFile {
apiVersion: string;
@@ -118,7 +118,7 @@ function normalizeHelmCharts(repoName: string, entries: RepoHelmChartList): Repo
Object.entries(entries),
([name, charts]) => [
name,
sortCharts(
sortBySemverVersion(
charts.map(chart => ({
...chart,
created: Date.parse(chart.created).toString(),
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import getHelmChartReadmeInjectable from "./get-helm-chart-readme.injectable";
export default getGlobalOverride(getHelmChartReadmeInjectable, () => () => {
throw new Error("tried to get a helm chart's readme without overriding");
});
@@ -7,8 +7,8 @@ import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-r
import type { HelmRepo } from "../../../common/helm/helm-repo";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
const getHelmChartInjectable = getInjectable({
id: "get-helm-chart",
const getHelmChartReadmeInjectable = getInjectable({
id: "get-helm-chart-readme",
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
@@ -21,16 +21,11 @@ const getHelmChartInjectable = getInjectable({
return undefined;
}
const chartManager = getChartManager(repo);
return {
readme: await chartManager.getReadme(chartName, version),
versions: await chartManager.chartVersions(chartName),
};
return getChartManager(repo).getReadme(chartName, version);
};
},
causesSideEffects: true,
});
export default getHelmChartInjectable;
export default getHelmChartReadmeInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmRepo } from "../../../common/helm/helm-repo";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable";
const getHelmChartVersionsInjectable = getInjectable({
id: "get-helm-chart-versions",
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo);
return async (repoName: string, chartName: string) => {
const repo = await getActiveHelmRepository(repoName);
if (!repo) {
return undefined;
}
return getChartManager(repo).chartVersions(chartName);
};
},
causesSideEffects: true,
});
export default getHelmChartVersionsInjectable;
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { apiBaseInjectionToken } from "../../common/k8s-api/api-base";
import createJsonApiInjectable from "../../common/k8s-api/create-json-api.injectable";
import { apiPrefix } from "../../common/vars";
import isDebuggingInjectable from "../../common/vars/is-debugging.injectable";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
const apiBaseInjectable = getInjectable({
id: "api-base",
instantiate: (di) => {
const proxyPort = di.inject(lensProxyPortInjectable);
const createJsonApi = di.inject(createJsonApiInjectable);
const isDebugging = di.inject(isDebuggingInjectable);
return createJsonApi({
serverAddress: `http://127.0.0.1:${proxyPort.get()}`,
apiBase: apiPrefix,
debug: di.inject(isDevelopmentInjectable) || isDebugging,
}, {
headers: {
"Host": `localhost:${proxyPort.get()}`,
},
});
},
injectionToken: apiBaseInjectionToken,
});
export default apiBaseInjectable;
@@ -6,7 +6,6 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { KubeAuthProxyDependencies } from "./kube-auth-proxy";
import { KubeAuthProxy } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster";
import path from "path";
import selfsigned from "selfsigned";
import { getBinaryName } from "../../common/vars";
import spawnInjectable from "../child-process/spawn.injectable";
@@ -14,6 +13,7 @@ import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
import loggerInjectable from "../../common/logger.injectable";
import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-port-is-used.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
@@ -22,11 +22,12 @@ const createKubeAuthProxyInjectable = getInjectable({
instantiate: (di): CreateKubeAuthProxy => {
const binaryName = getBinaryName("lens-k8s-proxy");
const joinPaths = di.inject(joinPathsInjectable);
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
const clusterUrl = new URL(cluster.apiUrl);
const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(baseBundledBinariesDirectoryInjectable), binaryName),
proxyBinPath: joinPaths(di.inject(baseBundledBinariesDirectoryInjectable), binaryName),
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
spawn: di.inject(spawnInjectable),
logger: di.inject(loggerInjectable),
@@ -9,6 +9,11 @@ import type { KubeconfigManagerDependencies } from "./kubeconfig-manager";
import { KubeconfigManager } from "./kubeconfig-manager";
import loggerInjectable from "../../common/logger.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import deleteFileInjectable from "../../common/fs/delete-file.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import writeFileInjectable from "../../common/fs/write-file.injectable";
export interface KubeConfigManagerInstantiationParameter {
cluster: Cluster;
@@ -24,6 +29,11 @@ const createKubeconfigManagerInjectable = getInjectable({
directoryForTemp: di.inject(directoryForTempInjectable),
logger: di.inject(loggerInjectable),
lensProxyPort: di.inject(lensProxyPortInjectable),
joinPaths: di.inject(joinPathsInjectable),
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
deleteFile: di.inject(deleteFileInjectable),
pathExists: di.inject(pathExistsInjectable),
writeFile: di.inject(writeFileInjectable),
};
return (cluster) => new KubeconfigManager(dependencies, cluster);
@@ -6,17 +6,25 @@
import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../common/cluster/cluster";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import path from "path";
import fs from "fs-extra";
import { dumpConfigYaml } from "../../common/kube-helpers";
import { isErrnoException } from "../../common/utils";
import type { PartialDeep } from "type-fest";
import type { Logger } from "../../common/logger";
import type { JoinPaths } from "../../common/path/join-paths.injectable";
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
import type { PathExists } from "../../common/fs/path-exists.injectable";
import type { DeleteFile } from "../../common/fs/delete-file.injectable";
import type { WriteFile } from "../../common/fs/write-file.injectable";
export interface KubeconfigManagerDependencies {
readonly directoryForTemp: string;
readonly logger: Logger;
lensProxyPort: { get: () => number };
readonly lensProxyPort: { get: () => number };
joinPaths: JoinPaths;
getDirnameOfPath: GetDirnameOfPath;
pathExists: PathExists;
deleteFile: DeleteFile;
writeFile: WriteFile;
}
export class KubeconfigManager {
@@ -24,10 +32,9 @@ export class KubeconfigManager {
* The path to the temp config file
*
* - if `string` then path
* - if `null` then not yet created
* - if `undefined` then unlinked by calling `clear()`
* - if `null` then not yet created or was cleared
*/
protected tempFilePath: string | null | undefined = null;
protected tempFilePath: string | null = null;
protected readonly contextHandler: ClusterContextHandler;
@@ -40,11 +47,7 @@ export class KubeconfigManager {
* @returns The path to the temporary kubeconfig
*/
async getPath(): Promise<string> {
if (this.tempFilePath === undefined) {
throw new Error("kubeconfig is already unlinked");
}
if (this.tempFilePath === null || !(await fs.pathExists(this.tempFilePath))) {
if (this.tempFilePath === null || !(await this.dependencies.pathExists(this.tempFilePath))) {
return await this.ensureFile();
}
@@ -62,13 +65,13 @@ export class KubeconfigManager {
this.dependencies.logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFilePath}`);
try {
await fs.unlink(this.tempFilePath);
await this.dependencies.deleteFile(this.tempFilePath);
} catch (error) {
if (isErrnoException(error) && error.code !== "ENOENT") {
throw error;
}
} finally {
this.tempFilePath = undefined;
this.tempFilePath = null;
}
}
@@ -78,7 +81,7 @@ export class KubeconfigManager {
return this.tempFilePath = await this.createProxyKubeconfig();
} catch (error) {
throw Object.assign(new Error("Failed to creat temp config for auth-proxy"), { cause: error });
throw new Error(`Failed to creat temp config for auth-proxy: ${error}`);
}
}
@@ -93,7 +96,7 @@ export class KubeconfigManager {
protected async createProxyKubeconfig(): Promise<string> {
const { cluster } = this;
const { contextName, id } = cluster;
const tempFile = path.join(
const tempFile = this.dependencies.joinPaths(
this.dependencies.directoryForTemp,
`kubeconfig-${id}`,
);
@@ -121,8 +124,7 @@ export class KubeconfigManager {
// write
const configYaml = dumpConfigYaml(proxyConfig);
await fs.ensureDir(path.dirname(tempFile));
await fs.writeFile(tempFile, configYaml, { mode: 0o600 });
await this.dependencies.writeFile(tempFile, configYaml, { mode: 0o600 });
this.dependencies.logger.debug(`[KUBECONFIG-MANAGER]: Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
return tempFile;
@@ -3,16 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
import kubectlBinaryNameInjectable from "./binary-name.injectable";
const bundledKubectlBinaryPathInjectable = getInjectable({
id: "bundled-kubectl-binary-path",
instantiate: (di) => path.join(
di.inject(baseBundledBinariesDirectoryInjectable),
di.inject(kubectlBinaryNameInjectable),
),
instantiate: (di) => {
const joinPaths = di.inject(joinPathsInjectable);
return joinPaths(
di.inject(baseBundledBinariesDirectoryInjectable),
di.inject(kubectlBinaryNameInjectable),
);
},
});
export default bundledKubectlBinaryPathInjectable;
@@ -14,6 +14,9 @@ import bundledKubectlBinaryPathInjectable from "./bundled-binary-path.injectable
import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
import bundledKubectlVersionInjectable from "../../common/vars/bundled-kubectl-version.injectable";
import kubectlVersionMapInjectable from "./version-map.injectable";
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
const createKubectlInjectable = getInjectable({
id: "create-kubectl",
@@ -29,6 +32,9 @@ const createKubectlInjectable = getInjectable({
baseBundeledBinariesDirectory: di.inject(baseBundledBinariesDirectoryInjectable),
bundledKubectlVersion: di.inject(bundledKubectlVersionInjectable),
kubectlVersionMap: di.inject(kubectlVersionMapInjectable),
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
joinPaths: di.inject(joinPathsInjectable),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
};
return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion);
+14 -9
View File
@@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import fs from "fs";
import { promiseExecFile } from "../../common/utils/promise-exec";
import logger from "../logger";
@@ -15,6 +14,9 @@ import got from "got/dist/source";
import { promisify } from "util";
import stream from "stream";
import { noop } from "lodash/fp";
import type { JoinPaths } from "../../common/path/join-paths.injectable";
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable";
const initScriptVersionString = "# lens-initscript v3";
@@ -33,6 +35,9 @@ export interface KubectlDependencies {
};
readonly bundledKubectlVersion: string;
readonly kubectlVersionMap: Map<string, string>;
joinPaths: JoinPaths;
getDirnameOfPath: GetDirnameOfPath;
getBasenameOfPath: GetBasenameOfPath;
}
export class Kubectl {
@@ -71,8 +76,8 @@ export class Kubectl {
}
this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${this.dependencies.normalizedDownloadPlatform}/${this.dependencies.normalizedDownloadArch}/${this.dependencies.kubectlBinaryName}`;
this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion));
this.path = path.join(this.dirname, this.dependencies.kubectlBinaryName);
this.dirname = this.dependencies.joinPaths(this.getDownloadDir(), this.kubectlVersion);
this.path = this.dependencies.joinPaths(this.dirname, this.dependencies.kubectlBinaryName);
}
public getBundledPath() {
@@ -85,7 +90,7 @@ export class Kubectl {
protected getDownloadDir() {
if (this.dependencies.userStore.downloadBinariesPath) {
return path.join(this.dependencies.userStore.downloadBinariesPath, "kubectl");
return this.dependencies.joinPaths(this.dependencies.userStore.downloadBinariesPath, "kubectl");
}
return this.dependencies.directoryForKubectlBinaries;
@@ -104,7 +109,7 @@ export class Kubectl {
if (!await this.checkBinary(this.getBundledPath(), false)) {
Kubectl.invalidBundle = true;
return path.basename(this.getBundledPath());
return this.dependencies.getBasenameOfPath(this.getBundledPath());
}
try {
@@ -246,7 +251,7 @@ export class Kubectl {
}
public async downloadKubectl() {
await ensureDir(path.dirname(this.path), 0o755);
await ensureDir(this.dependencies.getDirnameOfPath(this.path), 0o755);
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
@@ -268,9 +273,9 @@ export class Kubectl {
const binariesDir = this.dependencies.baseBundeledBinariesDirectory;
const kubectlPath = this.dependencies.userStore.downloadKubectlBinaries
? this.dirname
: path.dirname(this.getPathFromPreferences());
: this.dependencies.getDirnameOfPath(this.getPathFromPreferences());
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
const bashScriptPath = this.dependencies.joinPaths(this.dirname, ".bash_set_path");
const bashScript = [
initScriptVersionString,
"tempkubeconfig=\"$KUBECONFIG\"",
@@ -292,7 +297,7 @@ export class Kubectl {
"unset tempkubeconfig",
].join("\n");
const zshScriptPath = path.join(this.dirname, ".zlogin");
const zshScriptPath = this.dependencies.joinPaths(this.dirname, ".zlogin");
const zshScript = [
initScriptVersionString,
"tempkubeconfig=\"$KUBECONFIG\"",
@@ -4,15 +4,15 @@
*/
import { getInjectable } 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";
import clusterManagerInjectable from "../../../cluster/manager.injectable";
import openShellSessionInjectable from "../../../shell-session/create-shell-session.injectable";
const shellApiRequestInjectable = getInjectable({
id: "shell-api-request",
instantiate: (di) => shellApiRequest({
createShellSession: di.inject(createShellSessionInjectable),
openShellSession: di.inject(openShellSessionInjectable),
authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate,
clusterManager: di.inject(clusterManagerInjectable),
}),
@@ -4,28 +4,20 @@
*/
import logger from "../../../logger";
import type WebSocket from "ws";
import { Server as WebSocketServer } from "ws";
import type { ProxyApiRequestArgs } from "../types";
import type { ClusterManager } from "../../../cluster/manager";
import URLParse from "url-parse";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { ClusterId } from "../../../../common/cluster-types";
import type { OpenShellSession } from "../../../shell-session/create-shell-session.injectable";
interface Dependencies {
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
createShellSession: (args: {
webSocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName?: string;
}) => { open: () => Promise<void> };
openShellSession: OpenShellSession;
clusterManager: ClusterManager;
}
export const shellApiRequest = ({ createShellSession, authenticateRequest, clusterManager }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
export const shellApiRequest = ({ openShellSession, authenticateRequest, clusterManager }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
const cluster = clusterManager.getClusterForRequest(req);
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
@@ -37,10 +29,8 @@ export const shellApiRequest = ({ createShellSession, authenticateRequest, clust
const ws = new WebSocketServer({ noServer: true });
ws.handleUpgrade(req, socket, head, (webSocket) => {
const shell = createShellSession({ webSocket, cluster, tabId, nodeName });
shell.open()
ws.handleUpgrade(req, socket, head, (websocket) => {
openShellSession({ websocket, cluster, tabId, nodeName })
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
});
};
+9 -12
View File
@@ -4,7 +4,6 @@
*/
import type { Cluster } from "../common/cluster/cluster";
import type { KubernetesObject } from "@kubernetes/client-node";
import { exec } from "child_process";
import fs from "fs-extra";
import * as yaml from "js-yaml";
@@ -15,6 +14,7 @@ import { appEventBus } from "../common/app-event-bus/event-bus";
import { isChildProcessError } from "../common/utils";
import type { Patch } from "rfc6902";
import { promiseExecFile } from "../common/utils/promise-exec";
import type { KubernetesObject } from "@kubernetes/client-node";
export class ResourceApplier {
constructor(protected cluster: Cluster) {}
@@ -62,11 +62,12 @@ export class ResourceApplier {
}
}
async apply(resource: KubernetesObject | any): Promise<string> {
resource = this.sanitizeObject(resource);
async create(resource: string): Promise<string> {
appEventBus.emit({ name: "resource", action: "apply" });
return this.kubectlApply(yaml.dump(resource));
console.log({ resource });
return this.kubectlApply(this.sanitizeObject(resource));
}
protected async kubectlApply(content: string): Promise<string> {
@@ -149,17 +150,13 @@ export class ResourceApplier {
});
}
protected sanitizeObject(resource: KubernetesObject | any) {
const res = JSON.parse(JSON.stringify(resource));
protected sanitizeObject(resource: string) {
const res = yaml.load(resource) as Partial<KubernetesObject> & { status?: object };
delete res.status;
delete res.metadata?.resourceVersion;
const annotations = res.metadata?.annotations;
delete res.metadata?.annotations?.["kubectl.kubernetes.io/last-applied-configuration"];
if (annotations) {
delete annotations["kubectl.kubernetes.io/last-applied-configuration"];
}
return res;
return yaml.dump(res);
}
}
@@ -5,22 +5,22 @@
import { getRouteInjectable } from "../../../router/router.injectable";
import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import getHelmChartInjectable from "../../../helm/helm-service/get-helm-chart.injectable";
import getHelmChartReadmeInjectable from "../../../helm/helm-service/get-helm-chart-readme.injectable";
const getChartRouteInjectable = getRouteInjectable({
id: "get-chart-route",
const getHelmChartReadmeRouteInjectable = getRouteInjectable({
id: "get-helm-chart-readme-route",
instantiate: (di) => {
const getHelmChart = di.inject(getHelmChartInjectable);
const getHelmChartReadme = di.inject(getHelmChartReadmeInjectable);
return route({
method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}`,
path: `${apiPrefix}/v2/charts/{repo}/{chart}/readme`,
})(async ({ params, query }) => {
const { repo, chart } = params;
return {
response: await getHelmChart(
response: await getHelmChartReadme(
repo,
chart,
query.get("version") ?? undefined,
@@ -30,4 +30,4 @@ const getChartRouteInjectable = getRouteInjectable({
},
});
export default getChartRouteInjectable;
export default getHelmChartReadmeRouteInjectable;
@@ -7,8 +7,8 @@ import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import getHelmChartValuesInjectable from "../../../helm/helm-service/get-helm-chart-values.injectable";
const getChartRouteValuesInjectable = getRouteInjectable({
id: "get-chart-route-values",
const getHelmChartRouteValuesInjectable = getRouteInjectable({
id: "get-helm-chart-values-route",
instantiate: (di) => {
const getHelmChartValues = di.inject(getHelmChartValuesInjectable);
@@ -26,4 +26,4 @@ const getChartRouteValuesInjectable = getRouteInjectable({
},
});
export default getChartRouteValuesInjectable;
export default getHelmChartRouteValuesInjectable;
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../../router/router.injectable";
import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import getHelmChartVersionsInjectable from "../../../helm/helm-service/get-helm-chart-versions.injectable";
const getHelmChartVersionsRouteInjectable = getRouteInjectable({
id: "get-helm-chart-versions-route",
instantiate: (di) => {
const getHelmChartVersions = di.inject(getHelmChartVersionsInjectable);
return route({
method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}/versions`,
})(async ({ params: { repo, chart }}) => ({
response: await getHelmChartVersions(repo, chart),
}));
},
});
export default getHelmChartVersionsRouteInjectable;
@@ -7,8 +7,8 @@ import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import listHelmChartsInjectable from "../../../helm/helm-service/list-helm-charts.injectable";
const listChartsRouteInjectable = getRouteInjectable({
id: "list-charts-route",
const listHelmChartsRouteInjectable = getRouteInjectable({
id: "list-helm-charts-route",
instantiate: (di) => {
const listHelmCharts = di.inject(listHelmChartsInjectable);
@@ -22,4 +22,4 @@ const listChartsRouteInjectable = getRouteInjectable({
},
});
export default listChartsRouteInjectable;
export default listHelmChartsRouteInjectable;
@@ -9,6 +9,7 @@ import type { Cluster } from "../../../common/cluster/cluster";
import type { V1Secret } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node";
import { clusterRoute } from "../../router/route";
import { dump } from "js-yaml";
const getServiceAccountRouteInjectable = getRouteInjectable({
id: "get-service-account-route",
@@ -50,43 +51,15 @@ const getServiceAccountRouteInjectable = getRouteInjectable({
export default getServiceAccountRouteInjectable;
interface ServiceAccountKubeConfig {
apiVersion: string;
kind: string;
clusters: {
name: string;
cluster: {
server: string;
"certificate-authority-data": string;
};
}[];
users: {
name: string;
user: {
token: string;
};
}[];
contexts: {
name: string;
context: {
user: string;
cluster: string;
namespace: string | undefined;
};
}[];
"current-context": string;
}
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster): ServiceAccountKubeConfig | undefined {
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster): string | undefined {
if (!secret.data || !secret.metadata) {
return undefined;
}
const { token, "ca.crt": caCrt } = secret.data;
const tokenData = Buffer.from(token, "base64");
return {
return dump({
"apiVersion": "v1",
"kind": "Config",
"clusters": [
@@ -117,5 +90,5 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
},
],
"current-context": cluster.contextName,
};
});
}
@@ -18,7 +18,7 @@ import getMetricsInjectable from "../../get-metrics.injectable";
// This is used for backoff retry tracking.
const ATTEMPTS = [false, false, false, false, true];
const loadMetricsFor = (getMetrics: GetMetrics) => async (promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record<string, string>): Promise<any[]> => {
const loadMetricsFor = (getMetrics: GetMetrics) => async (promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Partial<Record<string, string>>): Promise<any[]> => {
const queries = promQueries.map(p => p.trim());
const loaders = new Map<string, Promise<any>>();
@@ -59,7 +59,7 @@ const addMetricsRouteInjectable = getRouteInjectable({
const getMetrics = di.inject(getMetricsInjectable);
const loadMetrics = loadMetricsFor(getMetrics);
const queryParams = Object.fromEntries(query.entries());
const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {};
try {
@@ -5,17 +5,19 @@
import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars";
import { ResourceApplier } from "../../resource-applier";
import { clusterRoute } from "../../router/route";
import { payloadValidatedClusterRoute } from "../../router/route";
import Joi from "joi";
const applyResourceRouteInjectable = getRouteInjectable({
id: "apply-resource-route",
const createResourceRouteInjectable = getRouteInjectable({
id: "create-resource-route",
instantiate: () => clusterRoute({
instantiate: () => payloadValidatedClusterRoute({
method: "post",
path: `${apiPrefix}/stack`,
payloadValidator: Joi.string(),
})(async ({ cluster, payload }) => ({
response: await new ResourceApplier(cluster).apply(payload),
response: await new ResourceApplier(cluster).create(payload),
})),
});
export default applyResourceRouteInjectable;
export default createResourceRouteInjectable;
@@ -11,8 +11,6 @@ import path from "path";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import httpProxy from "http-proxy";
import readFileBufferInjectable from "../../common/fs/read-file-buffer.injectable";
import type { GetAbsolutePath } from "../../common/path/get-absolute-path.injectable";
import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable";
import type { JoinPaths } from "../../common/path/join-paths.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { webpackDevServerPort } from "../../../webpack/vars";
@@ -20,11 +18,13 @@ import type { LensApiRequest, RouteResponse } from "../router/route";
import { route } from "../router/route";
import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable";
import appNameInjectable from "../../common/vars/app-name.injectable";
import type { GetAbsolutePath } from "../../common/path/get-absolute-path.injectable";
import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable";
interface ProductionDependencies {
readFileBuffer: (path: string) => Promise<Buffer>;
getAbsolutePath: GetAbsolutePath;
joinPaths: JoinPaths;
getAbsolutePath: GetAbsolutePath;
staticFilesDirectory: string;
appName: string;
}
@@ -98,8 +98,8 @@ const staticFileRouteInjectable = getRouteInjectable({
instantiate: (di) => {
const isDevelopment = di.inject(isDevelopmentInjectable);
const readFileBuffer = di.inject(readFileBufferInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const appName = di.inject(appNameInjectable);
@@ -114,10 +114,10 @@ const staticFileRouteInjectable = getRouteInjectable({
})
: handleStaticFileInProduction({
readFileBuffer,
getAbsolutePath,
joinPaths,
staticFilesDirectory,
appName,
getAbsolutePath,
}),
);
},
@@ -5,25 +5,30 @@
import { getInjectable } 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";
import openLocalShellSessionInjectable from "./local-shell-session/open.injectable";
import openNodeShellSessionInjectable from "./node-shell-session/open.injectable";
interface Args {
webSocket: WebSocket;
export interface OpenShellSessionArgs {
websocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName?: string;
}
const createShellSessionInjectable = getInjectable({
id: "create-shell-session",
export type OpenShellSession = (args: OpenShellSessionArgs) => Promise<void>;
instantiate:
(di) =>
({ nodeName, ...rest }: Args) =>
!nodeName
? di.inject(localShellSessionInjectable, rest)
: di.inject(nodeShellSessionInjectable, { nodeName, ...rest }),
const openShellSessionInjectable = getInjectable({
id: "open-shell-session",
instantiate: (di): OpenShellSession => {
const openLocalShellSession = di.inject(openLocalShellSessionInjectable);
return ({ nodeName, ...args }) => (
nodeName
? di.inject(openNodeShellSessionInjectable, { nodeName, ...args })
: openLocalShellSession(args)
);
},
});
export default createShellSessionInjectable;
export default openShellSessionInjectable;
@@ -1,33 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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";
import terminalShellEnvModifiersInjectable from "../shell-env-modifier/terminal-shell-env-modify.injectable";
interface InstantiationParameter {
webSocket: WebSocket;
cluster: Cluster;
tabId: string;
}
const localShellSessionInjectable = getInjectable({
id: "local-shell-session",
instantiate: (di, { cluster, tabId, webSocket }: InstantiationParameter) => {
const createKubectl = di.inject(createKubectlInjectable);
const localShellEnvModify = di.inject(terminalShellEnvModifiersInjectable);
const kubectl = createKubectl(cluster.version);
return new LocalShellSession(localShellEnvModify, kubectl, webSocket, cluster, tabId);
},
lifecycle: lifecycleEnum.transient,
});
export default localShellSessionInjectable;
@@ -3,24 +3,32 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type WebSocket from "ws";
import path from "path";
import { UserStore } from "../../../common/user-store";
import type { Cluster } from "../../../common/cluster/cluster";
import type { ClusterId } from "../../../common/cluster-types";
import type { UserStore } from "../../../common/user-store";
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
import { ShellSession } from "../shell-session";
import type { Kubectl } from "../../kubectl/kubectl";
import { baseBinariesDir } from "../../../common/vars";
import type { ModifyTerminalShellEnv } from "../shell-env-modifier/modify-terminal-shell-env.injectable";
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
import type { GetDirnameOfPath } from "../../../common/path/get-dirname.injectable";
import type { GetBasenameOfPath } from "../../../common/path/get-basename.injectable";
export interface LocalShellSessionDependencies extends ShellSessionDependencies {
readonly directoryForBinaries: string;
readonly userStore: UserStore;
modifyTerminalShellEnv: ModifyTerminalShellEnv;
joinPaths: JoinPaths;
getDirnameOfPath: GetDirnameOfPath;
getBasenameOfPath: GetBasenameOfPath;
}
export class LocalShellSession extends ShellSession {
ShellType = "shell";
constructor(protected shellEnvModify: (clusterId: ClusterId, env: Record<string, string | undefined>) => Record<string, string | undefined>, kubectl: Kubectl, websocket: WebSocket, cluster: Cluster, terminalId: string) {
super(kubectl, websocket, cluster, terminalId);
constructor(protected readonly dependencies: LocalShellSessionDependencies, args: ShellSessionArgs) {
super(dependencies, args);
}
protected getPathEntries(): string[] {
return [baseBinariesDir.get()];
return [this.dependencies.directoryForBinaries];
}
protected get cwd(): string | undefined {
@@ -31,7 +39,7 @@ export class LocalShellSession extends ShellSession {
let env = await this.getCachedShellEnv();
// extensions can modify the env
env = this.shellEnvModify(this.cluster.id, env);
env = this.dependencies.modifyTerminalShellEnv(this.cluster.id, env);
const shell = env.PTYSHELL;
@@ -45,16 +53,18 @@ export class LocalShellSession extends ShellSession {
}
protected async getShellArgs(shell: string): Promise<string[]> {
const pathFromPreferences = UserStore.getInstance().kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = UserStore.getInstance().downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries
? await this.kubectlBinDirP
: this.dependencies.getDirnameOfPath(pathFromPreferences);
switch(path.basename(shell)) {
switch(this.dependencies.getBasenameOfPath(shell)) {
case "powershell.exe":
return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${baseBinariesDir.get()};$Env:PATH"}`];
return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.directoryForBinaries};$Env:PATH"}`];
case "bash":
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
return ["--init-file", this.dependencies.joinPaths(await this.kubectlBinDirP, ".bash_set_path")];
case "fish":
return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${baseBinariesDir.get()}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
case "zsh":
return ["--login"];
default:
@@ -0,0 +1,55 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { LocalShellSessionDependencies } from "./local-shell-session";
import { LocalShellSession } from "./local-shell-session";
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
import modifyTerminalShellEnvInjectable from "../shell-env-modifier/modify-terminal-shell-env.injectable";
import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import type WebSocket from "ws";
import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
export interface OpenLocalShellSessionArgs {
websocket: WebSocket;
cluster: Cluster;
tabId: string;
}
export type OpenLocalShellSession = (args: OpenLocalShellSessionArgs) => Promise<void>;
const openLocalShellSessionInjectable = getInjectable({
id: "open-local-shell-session",
instantiate: (di): OpenLocalShellSession => {
const createKubectl = di.inject(createKubectlInjectable);
const dependencies: LocalShellSessionDependencies = {
directoryForBinaries: di.inject(directoryForBinariesInjectable),
isMac: di.inject(isMacInjectable),
modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
userStore: di.inject(userStoreInjectable),
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
joinPaths: di.inject(joinPathsInjectable),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
};
return (args) => {
const kubectl = createKubectl(args.cluster.version);
const session = new LocalShellSession(dependencies, { kubectl, ...args });
return session.open();
};
},
});
export default openLocalShellSessionInjectable;
@@ -1,32 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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({
id: "node-shell-session",
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;
@@ -3,28 +3,34 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type WebSocket from "ws";
import { v4 as uuid } from "uuid";
import { Watch, CoreV1Api } from "@kubernetes/client-node";
import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../../common/cluster/cluster";
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
import { ShellOpenError, ShellSession } from "../shell-session";
import { get, once } from "lodash";
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
import logger from "../../logger";
import type { Kubectl } from "../../kubectl/kubectl";
import { TerminalChannels } from "../../../common/terminal/channels";
import type { CreateKubeJsonApiForCluster } from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable";
export interface NodeShellSessionArgs extends ShellSessionArgs {
nodeName: string;
}
export interface NodeShellSessionDependencies extends ShellSessionDependencies {
createKubeJsonApiForCluster: CreateKubeJsonApiForCluster;
}
export class NodeShellSession extends ShellSession {
ShellType = "node-shell";
protected readonly podName = `node-shell-${uuid()}`;
protected readonly nodeName: string;
protected readonly cwd: string | undefined = undefined;
constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) {
super(kubectl, socket, cluster, terminalId);
constructor(protected readonly dependencies: NodeShellSessionDependencies, { nodeName, ...args }: NodeShellSessionArgs) {
super(dependencies, args);
this.nodeName = nodeName;
}
public async open() {
@@ -35,7 +41,7 @@ export class NodeShellSession extends ShellSession {
const cleanup = once(() => {
coreApi
.deleteNamespacedPod(this.podName, "kube-system")
.catch(error => logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
.catch(error => this.dependencies.logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
});
this.websocket.once("close", cleanup);
@@ -63,7 +69,7 @@ export class NodeShellSession extends ShellSession {
const args = ["exec", "-i", "-t", "-n", "kube-system", this.podName, "--"];
const nodeApi = new NodeApi({
objectConstructor: Node,
request: KubeJsonApi.forCluster(this.cluster.id),
request: this.dependencies.createKubeJsonApiForCluster(this.cluster.id),
});
const node = await nodeApi.get({ name: this.nodeName });
@@ -75,7 +81,7 @@ export class NodeShellSession extends ShellSession {
switch (nodeOs) {
default:
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
this.dependencies.logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
// fallthrough
case "linux":
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
@@ -127,7 +133,7 @@ export class NodeShellSession extends ShellSession {
}
protected waitForRunningPod(kc: KubeConfig): Promise<void> {
logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
this.dependencies.logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
return new Promise((resolve, reject) => {
new Watch(kc)
@@ -146,19 +152,19 @@ export class NodeShellSession extends ShellSession {
},
// done callback is called if the watch terminates normally
(err) => {
logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`);
this.dependencies.logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`);
reject(err);
},
)
.then(req => {
setTimeout(() => {
logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`);
this.dependencies.logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`);
req.abort();
reject("Pod creation timed out");
}, 2 * 60 * 1000); // 2 * 60 * 1000
})
.catch(error => {
logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`);
this.dependencies.logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`);
reject(error);
});
});
@@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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 type { NodeShellSessionDependencies } from "./node-shell-session";
import { NodeShellSession } from "./node-shell-session";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import createKubeJsonApiForClusterInjectable from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable";
export interface NodeShellSessionArgs {
websocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName: string;
}
const openNodeShellSessionInjectable = getInjectable({
id: "open-node-shell-session",
instantiate: (di, params: NodeShellSessionArgs) => {
const createKubectl = di.inject(createKubectlInjectable);
const dependencies: NodeShellSessionDependencies = {
isMac: di.inject(isMacInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
createKubeJsonApiForCluster: di.inject(createKubeJsonApiForClusterInjectable),
};
const kubectl = createKubectl(params.cluster.version);
const session = new NodeShellSession(dependencies, { kubectl, ...params });
return session.open();
},
// NOTE: this must be transient to bypass the circular dependency of `createKubeJsonApiForClusterInjectable` on the lens proxy port
lifecycle: lifecycleEnum.transient,
});
export default openNodeShellSessionInjectable;
@@ -0,0 +1,49 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { ClusterId } from "../../../common/cluster-types";
import { isDefined } from "../../../common/utils";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
export type ModifyTerminalShellEnv = (clusterId: ClusterId, env: Partial<Record<string, string>>) => Partial<Record<string, string>>;
const modifyTerminalShellEnvInjectable = getInjectable({
id: "terminal-shell-env-modify",
instantiate: (di): ModifyTerminalShellEnv => {
const extensions = di.inject(mainExtensionsInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const terminalShellEnvModifiers = computed(() => (
extensions.get()
.map((extension) => extension.terminalShellEnvModifier)
.filter(isDefined)
));
return (clusterId, env) => {
const modifiers = terminalShellEnvModifiers.get();
if (modifiers.length === 0) {
return env;
}
const entity = catalogEntityRegistry.findById(clusterId);
if (entity) {
const ctx = { catalogEntity: entity };
// clone it so the passed value is not also modified
env = JSON.parse(JSON.stringify(env));
env = modifiers.reduce((env, modifier) => modifier(ctx, env), env);
}
return env;
};
},
});
export default modifyTerminalShellEnvInjectable;
@@ -1,42 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import type { ClusterId } from "../../../common/cluster-types";
import { isDefined } from "../../../common/utils";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
import type { CatalogEntityRegistry } from "../../catalog";
interface Dependencies {
extensions: IComputedValue<LensMainExtension[]>;
catalogEntityRegistry: CatalogEntityRegistry;
}
export const terminalShellEnvModify = ({ extensions, catalogEntityRegistry }: Dependencies) =>
(clusterId: ClusterId, env: Record<string, string | undefined>) => {
const terminalShellEnvModifiers = computed(() => (
extensions.get()
.map((extension) => extension.terminalShellEnvModifier)
.filter(isDefined)
))
.get();
if (terminalShellEnvModifiers.length === 0) {
return env;
}
const entity = catalogEntityRegistry.findById(clusterId);
if (entity) {
const ctx = { catalogEntity: entity };
// clone it so the passed value is not also modified
env = JSON.parse(JSON.stringify(env));
env = terminalShellEnvModifiers.reduce((env, modifier) => modifier(ctx, env), env);
}
return env;
};
@@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import { terminalShellEnvModify } from "./terminal-shell-env-modifiers";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
const terminalShellEnvModifyInjectable = getInjectable({
id: "terminal-shell-env-modify",
instantiate: (di) =>
terminalShellEnvModify({
extensions: di.inject(mainExtensionsInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
}),
});
export default terminalShellEnvModifyInjectable;
+34 -16
View File
@@ -11,14 +11,13 @@ import { app } from "electron";
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
import path from "path";
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/app-event-bus/event-bus";
import logger from "../logger";
import { stat } from "fs/promises";
import { getOrInsertWith } from "../../common/utils";
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
import type { Logger } from "../../common/logger";
export class ShellOpenError extends Error {
constructor(message: string, options?: ErrorOptions) {
@@ -104,6 +103,19 @@ export enum WebSocketCloseEvent {
TlsHandshake = 1015,
}
export interface ShellSessionDependencies {
readonly isWindows: boolean;
readonly isMac: boolean;
readonly logger: Logger;
}
export interface ShellSessionArgs {
kubectl: Kubectl;
websocket: WebSocket;
cluster: Cluster;
tabId: string;
}
export abstract class ShellSession {
abstract readonly ShellType: string;
@@ -130,6 +142,9 @@ export abstract class ShellSession {
protected readonly kubectlBinDirP: Promise<string>;
protected readonly kubeconfigPathP: Promise<string>;
protected readonly terminalId: string;
protected readonly kubectl: Kubectl;
protected readonly websocket: WebSocket;
protected readonly cluster: Cluster;
protected abstract get cwd(): string | undefined;
@@ -147,12 +162,15 @@ export abstract class ShellSession {
})
));
logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`);
this.dependencies.logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`);
return { shellProcess, resume };
}
constructor(protected readonly kubectl: Kubectl, protected readonly websocket: WebSocket, protected readonly cluster: Cluster, terminalId: string) {
constructor(protected readonly dependencies: ShellSessionDependencies, { kubectl, websocket, cluster, tabId: terminalId }: ShellSessionArgs) {
this.kubectl = kubectl;
this.websocket = websocket;
this.cluster = cluster;
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
this.kubectlBinDirP = this.kubectl.binDir();
this.terminalId = `${cluster.id}:${terminalId}`;
@@ -165,7 +183,7 @@ export abstract class ShellSession {
protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
const cwdOptions = [this.cwd];
if (isWindows) {
if (this.dependencies.isWindows) {
cwdOptions.push(
env.USERPROFILE,
os.homedir(),
@@ -177,7 +195,7 @@ export abstract class ShellSession {
os.homedir(),
);
if (isMac) {
if (this.dependencies.isMac) {
cwdOptions.push("/Users");
} else {
cwdOptions.push("/home");
@@ -214,7 +232,7 @@ export abstract class ShellSession {
this.running = true;
shellProcess.onData(data => this.send({ type: TerminalChannels.STDOUT, data }));
shellProcess.onExit(({ exitCode }) => {
logger.info(`[SHELL-SESSION]: shell has exited for ${this.terminalId} closed with exitcode=${exitCode}`);
this.dependencies.logger.info(`[SHELL-SESSION]: shell has exited for ${this.terminalId} closed with exitcode=${exitCode}`);
// This might already be false because of the kill() within the websocket.on("close") handler
if (this.running) {
@@ -232,11 +250,11 @@ export abstract class ShellSession {
this.websocket
.on("message", (rawData: unknown): void => {
if (!this.running) {
return void logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
return void this.dependencies.logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
}
if (!(rawData instanceof Buffer)) {
return void logger.error(`[SHELL-SESSION]: Received message non-buffer message.`, { rawData });
return void this.dependencies.logger.error(`[SHELL-SESSION]: Received message non-buffer message.`, { rawData });
}
const data = rawData.toString();
@@ -252,18 +270,18 @@ export abstract class ShellSession {
shellProcess.resize(message.data.width, message.data.height);
break;
case TerminalChannels.PING:
logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`);
this.dependencies.logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`);
break;
default:
logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message);
this.dependencies.logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message);
break;
}
} catch (error) {
logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error);
this.dependencies.logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error);
}
})
.once("close", code => {
logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
this.dependencies.logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
const stopShellSession = this.running
&& (
@@ -278,11 +296,11 @@ export abstract class ShellSession {
this.running = false;
try {
logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`);
this.dependencies.logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`);
shellProcess.kill();
ShellSession.processes.delete(this.terminalId);
} catch (error) {
logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error);
this.dependencies.logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error);
}
}
});
@@ -319,7 +337,7 @@ export abstract class ShellSession {
delete env.DEBUG; // don't pass DEBUG into shells
if (isWindows) {
if (this.dependencies.isWindows) {
env.SystemRoot = process.env.SystemRoot;
env.PTYSHELL = shell || "powershell.exe";
env.PATH = pathStr;
@@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import createLensWindowInjectable from "../application-window/create-lens-window.injectable";
import staticFilesDirectoryInjectable from "../../../../common/vars/static-files-directory.injectable";
import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
const splashWindowInjectable = getInjectable({
id: "splash-window",
@@ -13,8 +13,8 @@ const splashWindowInjectable = getInjectable({
instantiate: (di) => {
const createLensWindow = di.inject(createLensWindowInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const splashWindowFile = getAbsolutePath(staticFilesDirectory, "splash.html");
const joinPaths = di.inject(joinPathsInjectable);
const splashWindowFile = joinPaths(staticFilesDirectory, "splash.html");
return createLensWindow({
id: "splash",
@@ -3,34 +3,33 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
import staticFilesDirectoryInjectable from "../../../common/vars/static-files-directory.injectable";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import { camelCase, flow, upperFirst } from "lodash/fp";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
const upperCamelCase = flow(camelCase, upperFirst);
const getTrayIconPathInjectable = getInjectable({
id: "get-tray-icon-path",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const isDevelopment = di.inject(isDevelopmentInjectable);
const isMac = di.inject(isMacInjectable);
const baseIconDirectory = getAbsolutePath(
const baseIconDirectory = joinPaths(
staticFilesDirectory,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
);
const fileSuffix = isMac ? "Template.png" : ".png";
return (name: string) =>
getAbsolutePath(
baseIconDirectory,
`trayIcon${upperCamelCase(name)}${fileSuffix}`,
);
return (name: string) => joinPaths(
baseIconDirectory,
`trayIcon${upperCamelCase(name)}${fileSuffix}`,
);
},
});