mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)
* Turn on strict mode in tsconfig.json - Add route, clusterRoute, and payloadValidatedClusterRoute helper functions to improve types with backend routes - Turn on the following new lints: - react/jsx-first-prop-new-line - react/jsx-wrap-multilines - react/jsx-one-expression-per-line - react/jsx-max-props-per-line - react/jsx-indent - react/jsx-indent-props Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace KubeObject scope strings with enum Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert package.json version changes Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move hostedCluster(Id) Signed-off-by: Sebastian Malton <sebastian@malton.name> * change some type param names to be not single letters Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove copy-extension-themes Signed-off-by: Sebastian Malton <sebastian@malton.name> * add new make clean action Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build to not use webpack for generating types Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move fileNameMigration to index Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix ref logic error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts test by overriding createStorage to not touch file system Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster.test.ts tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube=api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixed hotbar-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kubeconfig-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix daemonset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicaset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulsets/dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicasets/scale-dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments/scale/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cronjob.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stateful-set.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployment.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix job.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix scroll-spy.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix welcome.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix verify-that-all-routes-have-route-component.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pod-tolerations.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * better fix for previous 3 fixes, plus also select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix app-paths.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix isReactNode typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sub-title.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix drawer.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix list-layout.tsx and header.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix error-boundary.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix upgrade-chart/store.ts and dock-tab.store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix install-chart/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix edit-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix create-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select-filter.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crd-list.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix wrong types for extensions Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency on catalogCategoryRegistry Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-kube Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors, most <Select /> errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixing more type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * some more fixing type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeApis to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * factor out into a common file all the exports Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeObjectStores to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unused legacy KubeApi globals Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bad previous commit Signed-off-by: Sebastian Malton <sebastian@malton.name> * more crash fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * try and fix behavioural tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-core.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-kubernetes-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-application-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-extension-specific-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-telemetry-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix closing-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts - Fix other type errors too Signed-off-by: Sebastian Malton <sebastian@malton.name> * final tweaks Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add more tsconfig files, fix bug in <Catalog> - Make all of history, navigation injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of kube-details-params/ and navigate/ to injectable - This fixes a runtime error that was encountered during testing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix runtime errors on renderer - remove all static uses of `createPageParam` (and then removed the legacy global) - Made LensRendererExtension and LensMainExtension just used dependencies and not the getLegacyDi - Fixed circular dep in extension-loader Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move registerStore calls to after injectMany Signed-off-by: Sebastian Malton <sebastian@malton.name> * replace all the rest of the legacy uses of apiManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stack overflow and cycles in DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix NamespaceSelectFilter not opening Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix WizardStep and AddNamespaceDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix KubeApi's not being registered Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup WindowManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * Proper fix for Wizard, fix NamespaceStore.subscribe Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite withTooltip to be more type correct - Fixes mobx related "too many recursive actions" error - Change all the uses of withTooltips to be functional components Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add e2e test to cover kube api registration Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup internal-commands Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove cast in <Animate> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix command-palette e2e test Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test name Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix code to help CodeQL scanner Signed-off-by: Sebastian Malton <sebastian@malton.name> * update intree extension lock files Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build-extensions picking wrong @types/react Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix tests from rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigSyncManager more injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crash in test mode for Dialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * make Select snapshots deterministic Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix new type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object.store.test.ts typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix merge build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix snapshots after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * reexport BaseKubeJsonApiObjectMetadata Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo in terminalSpawningPool Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove duplicate license header Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo to waitUntilDefined Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove iter use from getLegacyGlobalDiForExtensionApi Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove complex createStorage override Signed-off-by: Sebastian Malton <sebastian@malton.name> * override logger with mocks only when needed for tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove specialized overrideStore flags for getDiForUnitTesting Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary | undefined types from the exactOptionalFieldTypes experiment Signed-off-by: Sebastian Malton <sebastian@malton.name> * use more descriptive name for local test mocks Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary addition to 'make clean' target Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove oddity of KubeObjectStore.getById(undefined) being allowed Signed-off-by: Sebastian Malton <sebastian@malton.name> * rename KubeObject.getDescriptor in favour of name without fundemental JS meaning Signed-off-by: Sebastian Malton <sebastian@malton.name> * Simplify legacyRegisterApi when working in behaviour unit tests - Don't emit within main environment as there should be no auto registering there Signed-off-by: Sebastian Malton <sebastian@malton.name> * change confusing variable name in ReactiveDuration Signed-off-by: Sebastian Malton <sebastian@malton.name> * make visitor pattern more explicit for Entity contextMenuOpen Signed-off-by: Sebastian Malton <sebastian@malton.name> * toggleDetails -> toggleKubeDetailsPane is more specific Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove outdated comment Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bug where LensExtension dependencies are not set Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests from the revert of react 18 Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix more tests from merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typings with new is-compatible-extension tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * more type fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert in-tree extension versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve name of guarding injectable for stores and apis - New name better implies that it is just a guard state and does not do anything Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add helper for <Select>.isMulti for storing in a Set<Value> Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix is-compatible-extension.test.ts types Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
381d77c633
commit
dfcb7c3330
1062 files changed
+26512
-28689
No files matched your search
@@ -3,32 +3,6 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
const logger = {
|
||||
silly: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
crit: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock("winston", () => ({
|
||||
format: {
|
||||
colorize: jest.fn(),
|
||||
combine: jest.fn(),
|
||||
simple: jest.fn(),
|
||||
label: jest.fn(),
|
||||
timestamp: jest.fn(),
|
||||
printf: jest.fn(),
|
||||
splat: jest.fn(),
|
||||
},
|
||||
createLogger: jest.fn().mockReturnValue(logger),
|
||||
transports: {
|
||||
Console: jest.fn(),
|
||||
File: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../common/ipc");
|
||||
jest.mock("request");
|
||||
jest.mock("request-promise-native");
|
||||
@@ -43,6 +17,8 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster
|
||||
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||
import { parse } from "url";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
|
||||
@@ -83,9 +59,17 @@ describe("create clusters", () => {
|
||||
|
||||
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||
di.override(createContextHandlerInjectable, () => () => {
|
||||
throw new Error("you should never come here");
|
||||
});
|
||||
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
||||
restartServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
clusterUrl: parse(cluster.apiUrl),
|
||||
getApiTarget: jest.fn(),
|
||||
getPrometheusDetails: jest.fn(),
|
||||
resolveAuthProxyCa: jest.fn(),
|
||||
resolveAuthProxyUrl: jest.fn(),
|
||||
setupPrometheus: jest.fn(),
|
||||
ensureServer: jest.fn(),
|
||||
} as ClusterContextHandler));
|
||||
|
||||
createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
@@ -99,6 +83,7 @@ describe("create clusters", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cluster.disconnect();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
@@ -121,11 +106,6 @@ describe("create clusters", () => {
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
|
||||
cluster.contextHandler = {
|
||||
ensureServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
} as any;
|
||||
|
||||
jest.spyOn(cluster, "reconnect");
|
||||
jest.spyOn(cluster, "refreshConnectionStatus");
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||
import type { PrometheusService } from "../prometheus";
|
||||
import { PrometheusProvider, PrometheusProviderRegistry } from "../prometheus";
|
||||
import mockFs from "mock-fs";
|
||||
@@ -48,7 +48,7 @@ class TestProvider extends PrometheusProvider {
|
||||
throw new Error("getQuery is not implemented.");
|
||||
}
|
||||
|
||||
async getPrometheusService(): Promise<PrometheusService> {
|
||||
async getPrometheusService(): Promise<PrometheusService | undefined> {
|
||||
switch (this.alwaysFail) {
|
||||
case ServiceResult.Success:
|
||||
return {
|
||||
@@ -66,14 +66,14 @@ class TestProvider extends PrometheusProvider {
|
||||
}
|
||||
|
||||
const clusterStub = {
|
||||
getProxyKubeconfig: (): any => ({
|
||||
makeApiClient: (): any => undefined,
|
||||
getProxyKubeconfig: () => ({
|
||||
makeApiClient: (): void => undefined,
|
||||
}),
|
||||
apiUrl: "http://localhost:81",
|
||||
} as Cluster;
|
||||
} as unknown as Cluster;
|
||||
|
||||
describe("ContextHandler", () => {
|
||||
let createContextHandler: (cluster: Cluster) => ContextHandler;
|
||||
let createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@@ -119,7 +119,7 @@ describe("ContextHandler", () => {
|
||||
|
||||
expect(() => {
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise<PrometheusService> };
|
||||
|
||||
return contextHandler.getPrometheusService();
|
||||
}).rejects.toBeDefined();
|
||||
@@ -149,7 +149,7 @@ describe("ContextHandler", () => {
|
||||
}
|
||||
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise<PrometheusService> };
|
||||
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
@@ -180,7 +180,7 @@ describe("ContextHandler", () => {
|
||||
}
|
||||
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise<PrometheusService> };
|
||||
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
@@ -217,7 +217,7 @@ describe("ContextHandler", () => {
|
||||
}
|
||||
|
||||
// TODO: Unit test shouldn't access protected or private methods
|
||||
const contextHandler = createContextHandler(clusterStub) as any;
|
||||
const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise<PrometheusService> };
|
||||
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
@@ -233,7 +233,7 @@ describe("ContextHandler", () => {
|
||||
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 contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise<PrometheusService> };
|
||||
|
||||
const service = await contextHandler.getPrometheusService();
|
||||
|
||||
|
||||
@@ -142,69 +142,69 @@ describe("kube auth proxy tests", () => {
|
||||
beforeEach(async () => {
|
||||
mockedCP = mock<ChildProcess>();
|
||||
listeners = new EventEmitter();
|
||||
const stderr = mockedCP.stderr = mock<Readable>();
|
||||
const stdout = mockedCP.stdout = mock<Readable>();
|
||||
|
||||
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
|
||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
||||
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
||||
mockedCP.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
||||
listeners.on(event, listener);
|
||||
|
||||
return mockedCP;
|
||||
});
|
||||
mockedCP.stderr = mock<Readable>();
|
||||
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.on(`stderr/${event}`, listener);
|
||||
mockedCP.stderr.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.on(`stderr/${String(event)}`, listener);
|
||||
|
||||
return mockedCP.stderr;
|
||||
return stderr;
|
||||
});
|
||||
mockedCP.stderr.off.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stderr/${event}`, listener);
|
||||
mockedCP.stderr.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stderr/${String(event)}`, listener);
|
||||
|
||||
return mockedCP.stderr;
|
||||
return stderr;
|
||||
});
|
||||
mockedCP.stderr.removeListener.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stderr/${event}`, listener);
|
||||
mockedCP.stderr.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stderr/${String(event)}`, listener);
|
||||
|
||||
return mockedCP.stderr;
|
||||
return stderr;
|
||||
});
|
||||
mockedCP.stderr.once.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.once(`stderr/${event}`, listener);
|
||||
mockedCP.stderr.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.once(`stderr/${String(event)}`, listener);
|
||||
|
||||
return mockedCP.stderr;
|
||||
return stderr;
|
||||
});
|
||||
mockedCP.stderr.removeAllListeners.mockImplementation((event?: string): Readable => {
|
||||
listeners.removeAllListeners(event ?? `stderr/${event}`);
|
||||
mockedCP.stderr.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
|
||||
listeners.removeAllListeners(event ?? `stderr/${String(event)}`);
|
||||
|
||||
return mockedCP.stderr;
|
||||
return stderr;
|
||||
});
|
||||
mockedCP.stdout = mock<Readable>();
|
||||
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.on(`stdout/${event}`, listener);
|
||||
mockedCP.stdout.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.on(`stdout/${String(event)}`, listener);
|
||||
|
||||
if (event === "data") {
|
||||
listeners.emit("stdout/data", "Starting to serve on 127.0.0.1:9191");
|
||||
}
|
||||
|
||||
return mockedCP.stdout;
|
||||
return stdout;
|
||||
});
|
||||
mockedCP.stdout.once.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.once(`stdout/${event}`, listener);
|
||||
mockedCP.stdout.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.once(`stdout/${String(event)}`, listener);
|
||||
|
||||
return mockedCP.stdout;
|
||||
return stdout;
|
||||
});
|
||||
mockedCP.stdout.off.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stdout/${event}`, listener);
|
||||
mockedCP.stdout.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stdout/${String(event)}`, listener);
|
||||
|
||||
return mockedCP.stdout;
|
||||
return stdout;
|
||||
});
|
||||
mockedCP.stdout.removeListener.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stdout/${event}`, listener);
|
||||
mockedCP.stdout.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||
listeners.off(`stdout/${String(event)}`, listener);
|
||||
|
||||
return mockedCP.stdout;
|
||||
return stdout;
|
||||
});
|
||||
mockedCP.stdout.removeAllListeners.mockImplementation((event?: string): Readable => {
|
||||
listeners.removeAllListeners(event ?? `stdout/${event}`);
|
||||
mockedCP.stdout.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
|
||||
listeners.removeAllListeners(event ?? `stdout/${String(event)}`);
|
||||
|
||||
return mockedCP.stdout;
|
||||
return stdout;
|
||||
});
|
||||
mockSpawn.mockImplementationOnce((command: string): ChildProcess => {
|
||||
expect(path.basename(command).split(".")[0]).toBe("lens-k8s-proxy");
|
||||
|
||||
@@ -2,33 +2,6 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
const logger = {
|
||||
silly: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
crit: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock("winston", () => ({
|
||||
format: {
|
||||
colorize: jest.fn(),
|
||||
combine: jest.fn(),
|
||||
simple: jest.fn(),
|
||||
label: jest.fn(),
|
||||
timestamp: jest.fn(),
|
||||
padLevels: jest.fn(),
|
||||
ms: jest.fn(),
|
||||
printf: jest.fn(),
|
||||
splat: jest.fn(),
|
||||
},
|
||||
createLogger: jest.fn().mockReturnValue(logger),
|
||||
transports: {
|
||||
Console: jest.fn(),
|
||||
File: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||
@@ -43,19 +16,34 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster
|
||||
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
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";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
|
||||
describe("kubeconfig manager tests", () => {
|
||||
let clusterFake: Cluster;
|
||||
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
let di: DiContainer;
|
||||
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined;
|
||||
let di: DiContainer;
|
||||
let loggerMock: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
|
||||
|
||||
loggerMock = {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
silly: jest.fn(),
|
||||
};
|
||||
|
||||
di.override(loggerInjectable, () => loggerMock);
|
||||
|
||||
mockFs({
|
||||
"minikube-config.yml": JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
@@ -82,9 +70,17 @@ describe("kubeconfig manager tests", () => {
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
di.override(createContextHandlerInjectable, () => () => {
|
||||
throw new Error("you should never come here");
|
||||
});
|
||||
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
||||
restartServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
clusterUrl: parse(cluster.apiUrl),
|
||||
getApiTarget: jest.fn(),
|
||||
getPrometheusDetails: jest.fn(),
|
||||
resolveAuthProxyCa: jest.fn(),
|
||||
resolveAuthProxyUrl: jest.fn(),
|
||||
setupPrometheus: jest.fn(),
|
||||
ensureServer: jest.fn(),
|
||||
}));
|
||||
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
@@ -96,10 +92,6 @@ describe("kubeconfig manager tests", () => {
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
|
||||
clusterFake.contextHandler = {
|
||||
ensureServer: () => Promise.resolve(),
|
||||
} as any;
|
||||
|
||||
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
|
||||
});
|
||||
|
||||
@@ -110,7 +102,9 @@ describe("kubeconfig manager tests", () => {
|
||||
it("should create 'temp' kube config with proxy", async () => {
|
||||
const kubeConfManager = createKubeconfigManager(clusterFake);
|
||||
|
||||
expect(logger.error).not.toBeCalled();
|
||||
assert(kubeConfManager, "should actually create one");
|
||||
|
||||
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());
|
||||
@@ -125,6 +119,8 @@ describe("kubeconfig manager tests", () => {
|
||||
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
|
||||
const kubeConfManager = createKubeconfigManager(clusterFake);
|
||||
|
||||
assert(kubeConfManager, "should actually create one");
|
||||
|
||||
const configPath = await kubeConfManager.getPath();
|
||||
|
||||
expect(await fse.pathExists(configPath)).toBe(true);
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
||||
|
||||
describe("clearKubeconfigEnvVars tests", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { LensApiRequest, Route } from "../router/router";
|
||||
import type { LensApiRequest, Route } from "../router/route";
|
||||
import staticFileRouteInjectable from "../routes/static-file-route.injectable";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
|
||||
@@ -24,7 +24,7 @@ jest.mock("electron", () => ({
|
||||
}));
|
||||
|
||||
describe("static-file-route", () => {
|
||||
let handleStaticFileRoute: Route<Buffer>;
|
||||
let handleStaticFileRoute: Route<Buffer, "/{path*}">;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@@ -40,7 +40,7 @@ describe("static-file-route", () => {
|
||||
path: "../index.ts",
|
||||
},
|
||||
raw: {},
|
||||
} as LensApiRequest<any>;
|
||||
} as LensApiRequest<"/{path*}">;
|
||||
|
||||
const result = await handleStaticFileRoute.handler(request);
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("static-file-route", () => {
|
||||
path: "router.test.ts",
|
||||
},
|
||||
raw: { req },
|
||||
} as LensApiRequest<any>;
|
||||
} as LensApiRequest<"/{path*}">;
|
||||
|
||||
const result = await handleStaticFileRoute.handler(request);
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ import { getElectronAppPath } from "./get-electron-app-path";
|
||||
const getElectronAppPathInjectable = getInjectable({
|
||||
id: "get-electron-app-path",
|
||||
|
||||
instantiate: (di) =>
|
||||
getElectronAppPath({ app: di.inject(electronAppInjectable) }),
|
||||
instantiate: (di) => getElectronAppPath({
|
||||
app: di.inject(electronAppInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default getElectronAppPathInjectable;
|
||||
@@ -11,7 +11,7 @@ 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 | null;
|
||||
let getElectronAppPath: (name: string) => string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: false });
|
||||
@@ -37,7 +37,7 @@ describe("get-electron-app-path", () => {
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
getElectronAppPath = di.inject(getElectronAppPathInjectable);
|
||||
getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string;
|
||||
});
|
||||
|
||||
it("given app path exists, when called, returns app path", () => {
|
||||
|
||||
@@ -9,12 +9,16 @@ interface Dependencies {
|
||||
app: App;
|
||||
}
|
||||
|
||||
export const getElectronAppPath =
|
||||
({ app }: Dependencies) =>
|
||||
(name: PathName) : string | null => {
|
||||
try {
|
||||
return app.getPath(name);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
export type GetElectronAppPath = (name: PathName) => string;
|
||||
|
||||
export const getElectronAppPath = ({
|
||||
app,
|
||||
}: Dependencies): GetElectronAppPath => (
|
||||
(name) => {
|
||||
try {
|
||||
return app.getPath(name);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -15,7 +15,7 @@ import { ipcMain } from "electron";
|
||||
import { nextUpdateChannel } from "./utils/update-channel";
|
||||
import { UserStore } from "../common/user-store";
|
||||
|
||||
let installVersion: null | string = null;
|
||||
let installVersion: undefined | string;
|
||||
|
||||
export function isAutoUpdateEnabled() {
|
||||
return autoUpdater.isUpdaterActive() && isPublishConfigured;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ObservableMap } from "mobx";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { loadFromOptions } from "../../../common/kube-helpers";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync-manager/kubeconfig-sync-manager";
|
||||
import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync/manager";
|
||||
import mockFs from "mock-fs";
|
||||
import fs from "fs";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
@@ -53,7 +53,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
});
|
||||
|
||||
di.override(clusterStoreInjectable, () =>
|
||||
ClusterStore.createInstance({ createCluster: () => null }),
|
||||
ClusterStore.createInstance({ createCluster: () => null as never }),
|
||||
);
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
+3
-1
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
import { getInjectable } 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 { KubeconfigSyncManager } from "./manager";
|
||||
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
||||
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
|
||||
|
||||
const kubeconfigSyncManagerInjectable = getInjectable({
|
||||
id: "kubeconfig-sync-manager",
|
||||
@@ -13,6 +14,7 @@ const kubeconfigSyncManagerInjectable = getInjectable({
|
||||
instantiate: (di) => new KubeconfigSyncManager({
|
||||
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
+13
-11
@@ -6,9 +6,9 @@
|
||||
import type { IComputedValue, ObservableMap } from "mobx";
|
||||
import { action, observable, computed, runInAction, makeObservable, observe } from "mobx";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { catalogEntityRegistry } from "../../catalog";
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import { watch } from "chokidar";
|
||||
import type { Stats } from "fs";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type stream from "stream";
|
||||
@@ -26,6 +26,7 @@ import globToRegExp from "glob-to-regexp";
|
||||
import { inspect } from "util";
|
||||
import type { ClusterModel, UpdateClusterModel } from "../../../common/cluster-types";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import type { CatalogEntityRegistry } from "../../catalog/entity-registry";
|
||||
|
||||
const logPrefix = "[KUBECONFIG-SYNC]:";
|
||||
|
||||
@@ -51,18 +52,19 @@ const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
|
||||
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
|
||||
|
||||
interface Dependencies {
|
||||
directoryForKubeConfigs: string;
|
||||
readonly directoryForKubeConfigs: string;
|
||||
readonly entityRegistry: CatalogEntityRegistry;
|
||||
createCluster: (model: ClusterModel) => Cluster;
|
||||
}
|
||||
|
||||
const kubeConfigSyncName = "lens:kube-sync";
|
||||
|
||||
export class KubeconfigSyncManager {
|
||||
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
||||
protected readonly sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
||||
protected syncing = false;
|
||||
protected syncListDisposer?: Disposer;
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@@ -76,7 +78,7 @@ export class KubeconfigSyncManager {
|
||||
|
||||
logger.info(`${logPrefix} starting requested syncs`);
|
||||
|
||||
catalogEntityRegistry.addComputedSource(kubeConfigSyncName, computed(() => (
|
||||
this.dependencies.entityRegistry.addComputedSource(kubeConfigSyncName, computed(() => (
|
||||
Array.from(iter.flatMap(
|
||||
this.sources.values(),
|
||||
([entities]) => entities.get(),
|
||||
@@ -110,7 +112,7 @@ export class KubeconfigSyncManager {
|
||||
this.stopOldSync(filePath);
|
||||
}
|
||||
|
||||
catalogEntityRegistry.removeSource(kubeConfigSyncName);
|
||||
this.dependencies.entityRegistry.removeSource(kubeConfigSyncName);
|
||||
this.syncing = false;
|
||||
}
|
||||
|
||||
@@ -164,7 +166,7 @@ type RootSourceValue = [Cluster, CatalogEntity];
|
||||
type RootSource = ObservableMap<string, RootSourceValue>;
|
||||
|
||||
// exported for testing
|
||||
export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Dependencies) => (contents: string, source: RootSource, filePath: string): void => {
|
||||
export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Pick<Dependencies, "createCluster" | "directoryForKubeConfigs">) => (contents: string, source: RootSource, filePath: string): void => {
|
||||
runInAction(() => {
|
||||
try {
|
||||
const { config, error } = loadConfigFromString(contents);
|
||||
@@ -226,7 +228,6 @@ export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Dependen
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
@@ -322,12 +323,13 @@ const watchFileChanges = (filePath: string, dependencies: Dependencies): [ICompu
|
||||
stabilityThreshold: 1000,
|
||||
},
|
||||
atomic: 150, // for "atomic writes"
|
||||
alwaysStat: true,
|
||||
});
|
||||
|
||||
const diffChangedConfig = diffChangedConfigFor(dependencies);
|
||||
|
||||
watcher
|
||||
.on("change", (childFilePath, stats) => {
|
||||
.on("change", (childFilePath, stats: Stats): void => {
|
||||
const cleanup = cleanupFns.get(childFilePath);
|
||||
|
||||
if (!cleanup) {
|
||||
@@ -343,7 +345,7 @@ const watchFileChanges = (filePath: string, dependencies: Dependencies): [ICompu
|
||||
maxAllowedFileReadSize,
|
||||
}));
|
||||
})
|
||||
.on("add", (childFilePath, stats) => {
|
||||
.on("add", (childFilePath, stats: Stats): void => {
|
||||
if (isFolderSync) {
|
||||
const fileName = path.basename(childFilePath);
|
||||
|
||||
@@ -368,7 +370,7 @@ const watchFileChanges = (filePath: string, dependencies: Dependencies): [ICompu
|
||||
})
|
||||
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
|
||||
} catch (error) {
|
||||
console.log(error.stack);
|
||||
console.log((error as { stack: unknown }).stack);
|
||||
logger.warn(`${logPrefix} failed to start watching changes: ${error}`);
|
||||
}
|
||||
})();
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import catalogEntityRegistryInjectable from "../catalog/catalog-entity-registry.injectable";
|
||||
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
|
||||
import { generalCatalogEntityInjectionToken } from "../../common/catalog-entities/general-catalog-entities/general-catalog-entity-injection-token";
|
||||
import { computed } from "mobx";
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import { observable, reaction } from "mobx";
|
||||
import type { WebLinkSpec, WebLinkStatus } from "../../../common/catalog-entities";
|
||||
import { WebLink } from "../../../common/catalog-entities";
|
||||
import type { CatalogEntityMetadata } from "../../../common/catalog";
|
||||
import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
|
||||
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||
import { CatalogEntity } from "../../../common/catalog";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import catalogEntityRegistryInjectable from "../entity-registry.injectable";
|
||||
import type { CatalogEntityRegistry } from "../entity-registry";
|
||||
|
||||
class InvalidEntity extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
|
||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
@@ -32,7 +34,7 @@ class InvalidEntity extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
||||
}
|
||||
|
||||
describe("CatalogEntityRegistry", () => {
|
||||
let registry: CatalogEntityRegistry;
|
||||
let entityRegistry: CatalogEntityRegistry;
|
||||
const entity = new WebLink({
|
||||
metadata: {
|
||||
uid: "test",
|
||||
@@ -63,26 +65,28 @@ describe("CatalogEntityRegistry", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
});
|
||||
|
||||
describe("addSource", () => {
|
||||
it ("allows to add an observable source", () => {
|
||||
const source = observable.array([]);
|
||||
const source = observable.array<WebLink>([]);
|
||||
|
||||
registry.addObservableSource("test", source);
|
||||
expect(registry.items.length).toEqual(0);
|
||||
entityRegistry.addObservableSource("test", source);
|
||||
expect(entityRegistry.items.length).toEqual(0);
|
||||
|
||||
source.push(entity);
|
||||
|
||||
expect(registry.items.length).toEqual(1);
|
||||
expect(entityRegistry.items.length).toEqual(1);
|
||||
});
|
||||
|
||||
it ("added source change triggers reaction", (done) => {
|
||||
const source = observable.array([]);
|
||||
const source = observable.array<WebLink>([]);
|
||||
|
||||
registry.addObservableSource("test", source);
|
||||
reaction(() => registry.items, () => {
|
||||
entityRegistry.addObservableSource("test", source);
|
||||
reaction(() => entityRegistry.items, () => {
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -92,31 +96,31 @@ describe("CatalogEntityRegistry", () => {
|
||||
|
||||
describe("removeSource", () => {
|
||||
it ("removes source", () => {
|
||||
const source = observable.array([]);
|
||||
const source = observable.array<WebLink>([]);
|
||||
|
||||
registry.addObservableSource("test", source);
|
||||
entityRegistry.addObservableSource("test", source);
|
||||
source.push(entity);
|
||||
registry.removeSource("test");
|
||||
entityRegistry.removeSource("test");
|
||||
|
||||
expect(registry.items.length).toEqual(0);
|
||||
expect(entityRegistry.items.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("items", () => {
|
||||
it("returns added items", () => {
|
||||
expect(registry.items.length).toBe(0);
|
||||
expect(entityRegistry.items.length).toBe(0);
|
||||
|
||||
const source = observable.array([entity]);
|
||||
|
||||
registry.addObservableSource("test", source);
|
||||
expect(registry.items.length).toBe(1);
|
||||
entityRegistry.addObservableSource("test", source);
|
||||
expect(entityRegistry.items.length).toBe(1);
|
||||
});
|
||||
|
||||
it("does not return items without matching category", () => {
|
||||
const source = observable.array([invalidEntity]);
|
||||
|
||||
registry.addObservableSource("test", source);
|
||||
expect(registry.items.length).toBe(0);
|
||||
entityRegistry.addObservableSource("test", source);
|
||||
expect(entityRegistry.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,50 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, computed, type IComputedValue, type IObservableArray, makeObservable, observable } from "mobx";
|
||||
import type { CatalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor } from "../../common/catalog";
|
||||
import { catalogCategoryRegistry } from "../../common/catalog";
|
||||
import { iter } from "../../common/utils";
|
||||
import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import catalogEntityRegistryInjectable from "./entity-registry.injectable";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>();
|
||||
|
||||
constructor(private categoryRegistry: CatalogCategoryRegistry) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
this.sources.set(id, computed(() => source));
|
||||
}
|
||||
|
||||
@action addComputedSource(id: string, source: IComputedValue<CatalogEntity[]>) {
|
||||
this.sources.set(id, source);
|
||||
}
|
||||
|
||||
@action removeSource(id: string) {
|
||||
this.sources.delete(id);
|
||||
}
|
||||
|
||||
@computed get items(): CatalogEntity[] {
|
||||
return Array.from(
|
||||
iter.filter(
|
||||
iter.flatMap(this.sources.values(), source => source.get()),
|
||||
entity => this.categoryRegistry.getCategoryForEntity(entity),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getById<T extends CatalogEntity>(id: string): T | undefined {
|
||||
return this.items.find(entity => entity.getId() === id) as T | undefined;
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
|
||||
}
|
||||
|
||||
getItemsByEntityClass<T extends CatalogEntity>(constructor: CatalogEntityConstructor<T>): T[] {
|
||||
return this.items.filter((item) => item instanceof constructor) as T[];
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
/**
|
||||
* @deprecated use `di.inject(catalogEntityRegistryInjectable)` instead
|
||||
*/
|
||||
export const catalogEntityRegistry = asLegacyGlobalForExtensionApi(catalogEntityRegistryInjectable);
|
||||
+5
-3
@@ -3,12 +3,14 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { catalogEntityRegistry } from "./catalog-entity-registry";
|
||||
import hasCategoryForEntityInjectable from "../../common/catalog/has-category-for-entity.injectable";
|
||||
import { CatalogEntityRegistry } from "./entity-registry";
|
||||
|
||||
const catalogEntityRegistryInjectable = getInjectable({
|
||||
id: "catalog-entity-registry",
|
||||
instantiate: () => catalogEntityRegistry,
|
||||
causesSideEffects: true,
|
||||
instantiate: (di) => new CatalogEntityRegistry({
|
||||
hasCategoryForEntity: di.inject(hasCategoryForEntityInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default catalogEntityRegistryInjectable;
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, computed, type IComputedValue, type IObservableArray, makeObservable, observable } from "mobx";
|
||||
import type { CatalogEntity } from "../../common/catalog";
|
||||
import type { HasCategoryForEntity } from "../../common/catalog/has-category-for-entity.injectable";
|
||||
import { iter } from "../../common/utils";
|
||||
|
||||
interface Dependencies {
|
||||
readonly hasCategoryForEntity: HasCategoryForEntity;
|
||||
}
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>();
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
this.sources.set(id, computed(() => source));
|
||||
}
|
||||
|
||||
@action addComputedSource(id: string, source: IComputedValue<CatalogEntity[]>) {
|
||||
this.sources.set(id, source);
|
||||
}
|
||||
|
||||
@action removeSource(id: string) {
|
||||
this.sources.delete(id);
|
||||
}
|
||||
|
||||
@computed get items(): CatalogEntity[] {
|
||||
return Array.from(
|
||||
iter.filter(
|
||||
iter.flatMap(this.sources.values(), source => source.get()),
|
||||
entity => this.dependencies.hasCategoryForEntity(entity),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
findById(id: string): CatalogEntity | undefined {
|
||||
return this.items.find(entity => entity.getId() === id);
|
||||
}
|
||||
|
||||
filterItemsForApiKind(apiVersion: string, kind: string): CatalogEntity[] {
|
||||
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
||||
}
|
||||
|
||||
filterItemsByPredicate<E extends CatalogEntity>(filter: (item: CatalogEntity) => item is E): E[] {
|
||||
return this.items.filter(filter);
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@
|
||||
*/
|
||||
|
||||
export * from "./catalog-entity-registry";
|
||||
export * from "./entity-registry";
|
||||
@@ -5,12 +5,12 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { spawn } from "child_process";
|
||||
|
||||
export type Spawn = typeof spawn;
|
||||
|
||||
const spawnInjectable = getInjectable({
|
||||
id: "spawn",
|
||||
|
||||
instantiate: () => {
|
||||
return spawn;
|
||||
},
|
||||
instantiate: (): Spawn => spawn,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -12,15 +12,12 @@ export interface ClusterDetectionResult {
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
export class BaseClusterDetector {
|
||||
key: string;
|
||||
export abstract class BaseClusterDetector {
|
||||
abstract readonly key: string;
|
||||
|
||||
constructor(public cluster: Cluster) {
|
||||
}
|
||||
constructor(public readonly cluster: Cluster) {}
|
||||
|
||||
detect(): Promise<ClusterDetectionResult> {
|
||||
return null;
|
||||
}
|
||||
abstract detect(): Promise<ClusterDetectionResult | null>;
|
||||
|
||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
return k8sRequest(this.cluster, path, options);
|
||||
|
||||
@@ -9,10 +9,12 @@ import { Singleton } from "../../common/utils";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
||||
|
||||
export class DetectorRegistry extends Singleton {
|
||||
registry = observable.array<typeof BaseClusterDetector>([], { deep: false });
|
||||
export type DetectorConstructor = new (cluster: Cluster) => BaseClusterDetector;
|
||||
|
||||
add(detectorClass: typeof BaseClusterDetector): this {
|
||||
export class DetectorRegistry extends Singleton {
|
||||
registry = observable.array<DetectorConstructor>([], { deep: false });
|
||||
|
||||
add(detectorClass: DetectorConstructor): this {
|
||||
this.registry.push(detectorClass);
|
||||
|
||||
return this;
|
||||
|
||||
@@ -8,12 +8,11 @@ import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
|
||||
export class DistributionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.DISTRIBUTION;
|
||||
version: string;
|
||||
|
||||
public async detect() {
|
||||
this.version = await this.getKubernetesVersion();
|
||||
const version = await this.getKubernetesVersion();
|
||||
|
||||
if (this.isRke()) {
|
||||
if (this.isRke(version)) {
|
||||
return { value: "rke", accuracy: 80 };
|
||||
}
|
||||
|
||||
@@ -21,19 +20,19 @@ export class DistributionDetector extends BaseClusterDetector {
|
||||
return { value: "rancher-desktop", accuracy: 80 };
|
||||
}
|
||||
|
||||
if (this.isK3s()) {
|
||||
if (this.isK3s(version)) {
|
||||
return { value: "k3s", accuracy: 80 };
|
||||
}
|
||||
|
||||
if (this.isGKE()) {
|
||||
if (this.isGKE(version)) {
|
||||
return { value: "gke", accuracy: 80 };
|
||||
}
|
||||
|
||||
if (this.isEKS()) {
|
||||
if (this.isEKS(version)) {
|
||||
return { value: "eks", accuracy: 80 };
|
||||
}
|
||||
|
||||
if (this.isIKS()) {
|
||||
if (this.isIKS(version)) {
|
||||
return { value: "iks", accuracy: 80 };
|
||||
}
|
||||
|
||||
@@ -45,27 +44,27 @@ export class DistributionDetector extends BaseClusterDetector {
|
||||
return { value: "digitalocean", accuracy: 90 };
|
||||
}
|
||||
|
||||
if (this.isK0s()) {
|
||||
if (this.isK0s(version)) {
|
||||
return { value: "k0s", accuracy: 80 };
|
||||
}
|
||||
|
||||
if (this.isVMWare()) {
|
||||
if (this.isVMWare(version)) {
|
||||
return { value: "vmware", accuracy: 90 };
|
||||
}
|
||||
|
||||
if (this.isMirantis()) {
|
||||
if (this.isMirantis(version)) {
|
||||
return { value: "mirantis", accuracy: 90 };
|
||||
}
|
||||
|
||||
if (this.isAlibaba()) {
|
||||
if (this.isAlibaba(version)) {
|
||||
return { value: "alibaba", accuracy: 90 };
|
||||
}
|
||||
|
||||
if (this.isHuawei()) {
|
||||
if (this.isHuawei(version)) {
|
||||
return { value: "huawei", accuracy: 90 };
|
||||
}
|
||||
|
||||
if (this.isTke()) {
|
||||
if (this.isTke(version)) {
|
||||
return { value: "tencent", accuracy: 90 };
|
||||
}
|
||||
|
||||
@@ -85,11 +84,11 @@ export class DistributionDetector extends BaseClusterDetector {
|
||||
return { value: "docker-desktop", accuracy: 80 };
|
||||
}
|
||||
|
||||
if (this.isCustom() && await this.isOpenshift()) {
|
||||
if (this.isCustom(version) && await this.isOpenshift()) {
|
||||
return { value: "openshift", accuracy: 90 };
|
||||
}
|
||||
|
||||
if (this.isCustom()) {
|
||||
if (this.isCustom(version)) {
|
||||
return { value: "custom", accuracy: 10 };
|
||||
}
|
||||
|
||||
@@ -104,24 +103,24 @@ export class DistributionDetector extends BaseClusterDetector {
|
||||
return response.gitVersion;
|
||||
}
|
||||
|
||||
protected isGKE() {
|
||||
return this.version.includes("gke");
|
||||
protected isGKE(version: string) {
|
||||
return version.includes("gke");
|
||||
}
|
||||
|
||||
protected isEKS() {
|
||||
return this.version.includes("eks");
|
||||
protected isEKS(version: string) {
|
||||
return version.includes("eks");
|
||||
}
|
||||
|
||||
protected isIKS() {
|
||||
return this.version.includes("IKS");
|
||||
protected isIKS(version: string) {
|
||||
return version.includes("IKS");
|
||||
}
|
||||
|
||||
protected isAKS() {
|
||||
return this.cluster.apiUrl.includes("azmk8s.io");
|
||||
}
|
||||
|
||||
protected isMirantis() {
|
||||
return this.version.includes("-mirantis-") || this.version.includes("-docker-");
|
||||
protected isMirantis(version: string) {
|
||||
return version.includes("-mirantis-") || version.includes("-docker-");
|
||||
}
|
||||
|
||||
protected isDigitalOcean() {
|
||||
@@ -144,40 +143,40 @@ export class DistributionDetector extends BaseClusterDetector {
|
||||
return this.cluster.contextName === "docker-desktop";
|
||||
}
|
||||
|
||||
protected isTke() {
|
||||
return this.version.includes("-tke.");
|
||||
protected isTke(version: string) {
|
||||
return version.includes("-tke.");
|
||||
}
|
||||
|
||||
protected isCustom() {
|
||||
return this.version.includes("+");
|
||||
protected isCustom(version: string) {
|
||||
return version.includes("+");
|
||||
}
|
||||
|
||||
protected isVMWare() {
|
||||
return this.version.includes("+vmware");
|
||||
protected isVMWare(version: string) {
|
||||
return version.includes("+vmware");
|
||||
}
|
||||
|
||||
protected isRke() {
|
||||
return this.version.includes("-rancher");
|
||||
protected isRke(version: string) {
|
||||
return version.includes("-rancher");
|
||||
}
|
||||
|
||||
protected isRancherDesktop() {
|
||||
return this.cluster.contextName === "rancher-desktop";
|
||||
}
|
||||
|
||||
protected isK3s() {
|
||||
return this.version.includes("+k3s");
|
||||
protected isK3s(version: string) {
|
||||
return version.includes("+k3s");
|
||||
}
|
||||
|
||||
protected isK0s() {
|
||||
return this.version.includes("-k0s") || this.version.includes("+k0s");
|
||||
protected isK0s(version: string) {
|
||||
return version.includes("-k0s") || version.includes("+k0s");
|
||||
}
|
||||
|
||||
protected isAlibaba() {
|
||||
return this.version.includes("-aliyun");
|
||||
protected isAlibaba(version: string) {
|
||||
return version.includes("-aliyun");
|
||||
}
|
||||
|
||||
protected isHuawei() {
|
||||
return this.version.includes("-CCE");
|
||||
protected isHuawei(version: string) {
|
||||
return version.includes("-CCE");
|
||||
}
|
||||
|
||||
protected async isOpenshift() {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
|
||||
export class VersionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.VERSION;
|
||||
value: string;
|
||||
|
||||
public async detect() {
|
||||
const version = await this.getKubernetesVersion();
|
||||
|
||||
@@ -9,10 +9,10 @@ import { action, makeObservable, observable, observe, reaction, toJS } from "mob
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { getClusterIdFromHost, Singleton } from "../common/utils";
|
||||
import { getClusterIdFromHost, isErrnoException, Singleton } from "../common/utils";
|
||||
import { catalogEntityRegistry } from "./catalog";
|
||||
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { once } from "lodash";
|
||||
import { ClusterStore } from "../common/cluster-store/cluster-store";
|
||||
@@ -49,12 +49,12 @@ export class ClusterManager extends Singleton {
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster) as KubernetesCluster[],
|
||||
() => catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
entities => this.syncClustersFromCatalog(entities),
|
||||
);
|
||||
|
||||
reaction(() => [
|
||||
catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster),
|
||||
catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
this.visibleCluster,
|
||||
] as const, ([entities, visibleCluster]) => {
|
||||
for (const entity of entities) {
|
||||
@@ -68,7 +68,7 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
observe(this.deleting, change => {
|
||||
if (change.type === "add") {
|
||||
this.updateEntityStatus(catalogEntityRegistry.getById(change.newValue));
|
||||
this.updateEntityStatus(catalogEntityRegistry.findById(change.newValue) as KubernetesCluster);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -197,7 +197,7 @@ export class ClusterManager extends Singleton {
|
||||
*/
|
||||
this.store.addCluster(model);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||
if (isErrnoException(error) && error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||
logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
|
||||
} else {
|
||||
logger.error(`${logPrefix} failed to add cluster: ${error}`, model);
|
||||
@@ -207,7 +207,7 @@ export class ClusterManager extends Singleton {
|
||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||
cluster.contextName = entity.spec.kubeconfigContext;
|
||||
|
||||
if (entity.spec.accessibleNamespace) {
|
||||
if (entity.spec.accessibleNamespaces) {
|
||||
cluster.accessibleNamespaces = entity.spec.accessibleNamespaces;
|
||||
}
|
||||
|
||||
@@ -258,9 +258,13 @@ export class ClusterManager extends Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
||||
getClusterForRequest(req: http.IncomingMessage): Cluster | undefined {
|
||||
if (!req.headers.host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||
if (req.url && req.headers.host.startsWith("127.0.0.1")) {
|
||||
const clusterId = req.url.split("/")[1];
|
||||
const cluster = this.store.getById(clusterId);
|
||||
|
||||
|
||||
@@ -32,8 +32,20 @@ interface Dependencies {
|
||||
authProxyCa: string;
|
||||
}
|
||||
|
||||
export class ContextHandler {
|
||||
public clusterUrl: UrlWithStringQuery;
|
||||
export interface ClusterContextHandler {
|
||||
readonly clusterUrl: UrlWithStringQuery;
|
||||
setupPrometheus(preferences?: ClusterPrometheusPreferences): void;
|
||||
getPrometheusDetails(): Promise<PrometheusDetails>;
|
||||
resolveAuthProxyUrl(): Promise<string>;
|
||||
resolveAuthProxyCa(): string;
|
||||
getApiTarget(isLongRunningRequest?: boolean): Promise<httpProxy.ServerOptions>;
|
||||
restartServer(): Promise<void>;
|
||||
ensureServer(): Promise<void>;
|
||||
stopServer(): void;
|
||||
}
|
||||
|
||||
export class ContextHandler implements ClusterContextHandler {
|
||||
public readonly clusterUrl: UrlWithStringQuery;
|
||||
protected kubeAuthProxy?: KubeAuthProxy;
|
||||
protected apiTarget?: httpProxy.ServerOptions;
|
||||
protected prometheusProvider?: string;
|
||||
@@ -46,7 +58,7 @@ export class ContextHandler {
|
||||
|
||||
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
||||
this.prometheusProvider = preferences.prometheusProvider?.type;
|
||||
this.prometheus = preferences.prometheus || null;
|
||||
this.prometheus = preferences.prometheus;
|
||||
}
|
||||
|
||||
public async getPrometheusDetails(): Promise<PrometheusDetails> {
|
||||
@@ -82,7 +94,7 @@ export class ContextHandler {
|
||||
}
|
||||
|
||||
protected async getPrometheusService(): Promise<PrometheusService> {
|
||||
if (this.prometheus !== null && this.prometheusProvider !== null) {
|
||||
if (this.prometheus && this.prometheusProvider) {
|
||||
return {
|
||||
id: this.prometheusProvider,
|
||||
namespace: this.prometheus.namespace,
|
||||
@@ -117,11 +129,11 @@ export class ContextHandler {
|
||||
throw Object.assign(new Error("No Prometheus service found"), { cause: errors });
|
||||
}
|
||||
|
||||
async resolveAuthProxyUrl() {
|
||||
await this.ensureServer();
|
||||
async resolveAuthProxyUrl(): Promise<string> {
|
||||
const kubeAuthProxy = await this.ensureServerHelper();
|
||||
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||
|
||||
return `https://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`;
|
||||
return `https://127.0.0.1:${kubeAuthProxy.port}${kubeAuthProxy.apiPrefix}${path}`;
|
||||
}
|
||||
|
||||
resolveAuthProxyCa() {
|
||||
@@ -139,30 +151,32 @@ export class ContextHandler {
|
||||
}
|
||||
|
||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||
await this.ensureServer();
|
||||
|
||||
const ca = this.dependencies.authProxyCa;
|
||||
const kubeAuthProxy = await this.ensureServerHelper();
|
||||
const ca = this.resolveAuthProxyCa();
|
||||
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
|
||||
const apiPrefix = `${kubeAuthProxy.apiPrefix}${clusterPath}`;
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (this.clusterUrl.hostname) {
|
||||
headers.Host = this.clusterUrl.hostname;
|
||||
}
|
||||
|
||||
return {
|
||||
target: {
|
||||
protocol: "https:",
|
||||
host: "127.0.0.1",
|
||||
port: this.kubeAuthProxy.port,
|
||||
port: kubeAuthProxy.port,
|
||||
path: apiPrefix,
|
||||
ca,
|
||||
},
|
||||
changeOrigin: true,
|
||||
timeout,
|
||||
secure: true,
|
||||
headers: {
|
||||
"Host": this.clusterUrl.hostname,
|
||||
},
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
async ensureServer() {
|
||||
protected async ensureServerHelper(): Promise<KubeAuthProxy> {
|
||||
if (!this.kubeAuthProxy) {
|
||||
const proxyEnv = Object.assign({}, process.env);
|
||||
|
||||
@@ -171,9 +185,23 @@ export class ContextHandler {
|
||||
}
|
||||
this.kubeAuthProxy = this.dependencies.createKubeAuthProxy(this.cluster, proxyEnv);
|
||||
await this.kubeAuthProxy.run();
|
||||
|
||||
return this.kubeAuthProxy;
|
||||
}
|
||||
|
||||
await this.kubeAuthProxy.whenReady;
|
||||
|
||||
return this.kubeAuthProxy;
|
||||
}
|
||||
|
||||
async ensureServer(): Promise<void> {
|
||||
await this.ensureServerHelper();
|
||||
}
|
||||
|
||||
async restartServer(): Promise<void> {
|
||||
this.stopServer();
|
||||
|
||||
await this.ensureServerHelper();
|
||||
}
|
||||
|
||||
stopServer() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import selfsigned from "selfsigned";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ClusterContextHandler } from "./context-handler";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate";
|
||||
@@ -14,15 +15,18 @@ const createContextHandlerInjectable = getInjectable({
|
||||
id: "create-context-handler",
|
||||
|
||||
instantiate: (di) => {
|
||||
return (cluster: Cluster) => {
|
||||
const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
|
||||
|
||||
return (cluster: Cluster): ClusterContextHandler | undefined => {
|
||||
const clusterUrl = new URLParse(cluster.apiUrl);
|
||||
|
||||
const dependencies = {
|
||||
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
|
||||
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
|
||||
};
|
||||
|
||||
return new ContextHandler(dependencies, cluster);
|
||||
return new ContextHandler(
|
||||
{
|
||||
createKubeAuthProxy,
|
||||
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
|
||||
},
|
||||
cluster,
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import createContextHandlerInjectable from "../context-handler/create-context-ha
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
|
||||
const createClusterInjectable = getInjectable({
|
||||
id: "create-cluster",
|
||||
@@ -24,6 +25,7 @@ const createClusterInjectable = getInjectable({
|
||||
createContextHandler: di.inject(createContextHandlerInjectable),
|
||||
createAuthorizationReview: di.inject(authorizationReviewInjectable),
|
||||
createListNamespaces: di.inject(listNamespacesInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
};
|
||||
|
||||
return (model) => new Cluster(dependencies, model);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 { createExtensionInstanceInjectionToken } from "../../extensions/extension-loader/create-extension-instance.token";
|
||||
import fileSystemProvisionerStoreInjectable from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
|
||||
import { lensExtensionDependencies } from "../../extensions/lens-extension";
|
||||
import type { LensMainExtensionDependencies } from "../../extensions/lens-extension-set-dependencies";
|
||||
import type { LensMainExtension } from "../../extensions/lens-main-extension";
|
||||
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
|
||||
|
||||
const createExtensionInstanceInjectable = getInjectable({
|
||||
id: "create-extension-instance",
|
||||
instantiate: (di) => {
|
||||
const deps: LensMainExtensionDependencies = {
|
||||
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
|
||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
};
|
||||
|
||||
return (ExtensionClass, extension) => {
|
||||
const instance = new ExtensionClass(extension) as LensMainExtension;
|
||||
|
||||
(instance as { [lensExtensionDependencies]: LensMainExtensionDependencies })[lensExtensionDependencies] = deps;
|
||||
|
||||
return instance;
|
||||
};
|
||||
},
|
||||
injectionToken: createExtensionInstanceInjectionToken,
|
||||
});
|
||||
|
||||
export default createExtensionInstanceInjectable;
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import glob from "glob";
|
||||
import { kebabCase, memoize, noop } from "lodash/fp";
|
||||
import { kebabCase, memoize } from "lodash/fp";
|
||||
import { createContainer } from "@ogre-tools/injectable";
|
||||
|
||||
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
@@ -20,13 +20,12 @@ import loggerInjectable from "../common/logger.injectable";
|
||||
import spawnInjectable from "./child-process/spawn.injectable";
|
||||
import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable";
|
||||
import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store";
|
||||
import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable";
|
||||
import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store";
|
||||
import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
|
||||
import type { FileSystemProvisionerStore } from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store";
|
||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||
import type { ClusterStore } from "../common/cluster-store/cluster-store";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
||||
import type { UserStore } from "../common/user-store";
|
||||
import isMacInjectable from "../common/vars/is-mac.injectable";
|
||||
import isWindowsInjectable from "../common/vars/is-windows.injectable";
|
||||
import isLinuxInjectable from "../common/vars/is-linux.injectable";
|
||||
@@ -34,11 +33,15 @@ import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectab
|
||||
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/hotbar-store.injectable";
|
||||
import hotbarStoreInjectable from "../common/hotbars/store.injectable";
|
||||
import type { GetDiForUnitTestingOptions } from "../test-utils/get-dis-for-unit-testing";
|
||||
import { noop } from "../renderer/utils";
|
||||
|
||||
export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
|
||||
const {
|
||||
doGeneralOverrides = false,
|
||||
} = opts;
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
) => {
|
||||
const di = createContainer();
|
||||
|
||||
setLegacyGlobalDiForExtensionApi(di, Environments.main);
|
||||
@@ -56,6 +59,11 @@ export const getDiForUnitTesting = (
|
||||
di.preventSideEffects();
|
||||
|
||||
if (doGeneralOverrides) {
|
||||
di.override(hotbarStoreInjectable, () => ({}));
|
||||
di.override(userStoreInjectable, () => ({}));
|
||||
di.override(extensionsStoreInjectable, () => ({ isEnabled: (opts) => (void opts, false) }) as ExtensionsStore);
|
||||
di.override(clusterStoreInjectable, () => ({ getById: (id) => (void id, {}) as Cluster }) as ClusterStore);
|
||||
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
|
||||
di.override(isMacInjectable, () => true);
|
||||
di.override(isWindowsInjectable, () => false);
|
||||
di.override(isLinuxInjectable, () => false);
|
||||
@@ -63,17 +71,6 @@ export const getDiForUnitTesting = (
|
||||
di.override(getAbsolutePathInjectable, () => getAbsolutePathFake);
|
||||
di.override(joinPathsInjectable, () => joinPathsFake);
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
|
||||
|
||||
di.override(hotbarStoreInjectable, () => ({}));
|
||||
|
||||
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore);
|
||||
di.override(userStoreInjectable, () => ({}) as UserStore);
|
||||
|
||||
di.override(
|
||||
getElectronAppPathInjectable,
|
||||
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
|
||||
@@ -83,13 +80,12 @@ export const getDiForUnitTesting = (
|
||||
di.override(appNameInjectable, () => "some-electron-app-name");
|
||||
di.override(registerChannelInjectable, () => () => undefined);
|
||||
di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory");
|
||||
|
||||
di.override(spawnInjectable, () => () => {
|
||||
return {
|
||||
stderr: { on: jest.fn(), removeAllListeners: jest.fn() },
|
||||
stdout: { on: jest.fn(), removeAllListeners: jest.fn() },
|
||||
on: jest.fn(),
|
||||
} as any;
|
||||
} as never;
|
||||
});
|
||||
|
||||
di.override(writeJsonFileInjectable, () => () => {
|
||||
@@ -107,13 +103,14 @@ export const getDiForUnitTesting = (
|
||||
di.override(loggerInjectable, () => ({
|
||||
warn: noop,
|
||||
debug: noop,
|
||||
error: (message: string, ...args: any) => console.error(message, ...args),
|
||||
error: noop,
|
||||
info: noop,
|
||||
silly: noop,
|
||||
}));
|
||||
}
|
||||
|
||||
return di;
|
||||
};
|
||||
}
|
||||
|
||||
const getInjectableFilePaths = memoize(() => [
|
||||
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||
|
||||
+13
-3
@@ -5,15 +5,21 @@
|
||||
|
||||
import { promiseExecFile } from "../../common/utils/promise-exec";
|
||||
import type { BaseEncodingOptions } from "fs";
|
||||
import type { ExecFileOptions } from "child_process";
|
||||
import type { ExecFileOptions, ExecFileOptionsWithStringEncoding } from "child_process";
|
||||
import { helmBinaryPath } from "../../common/vars";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { isChildProcessError } from "../../common/utils";
|
||||
|
||||
/**
|
||||
* ExecFile the bundled helm CLI
|
||||
* @returns STDOUT
|
||||
*/
|
||||
export async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
|
||||
export async function execHelm(args: string[], { encoding, ...rest }: BaseEncodingOptions & ExecFileOptions = {}): Promise<string> {
|
||||
const options: ExecFileOptionsWithStringEncoding = {
|
||||
encoding: encoding ?? "utf-8",
|
||||
...rest,
|
||||
};
|
||||
|
||||
try {
|
||||
const opts = { ...options };
|
||||
|
||||
@@ -27,6 +33,10 @@ export async function execHelm(args: string[], options?: BaseEncodingOptions & E
|
||||
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
if (isChildProcessError(error, "string")) {
|
||||
throw error.stderr || error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@ import * as yaml from "js-yaml";
|
||||
import type { HelmRepo } from "./helm-repo-manager";
|
||||
import logger from "../logger";
|
||||
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { iter, sortCharts } from "../../common/utils";
|
||||
import { iter, put, sortCharts } from "../../common/utils";
|
||||
import { execHelm } from "./exec";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import { assert } from "console";
|
||||
|
||||
interface ChartCacheEntry {
|
||||
data: Buffer;
|
||||
@@ -23,9 +25,15 @@ export interface HelmCacheFile {
|
||||
}
|
||||
|
||||
export class HelmChartManager {
|
||||
static #cache = new Map<string, ChartCacheEntry>();
|
||||
static readonly #cache = new Map<string, ChartCacheEntry>();
|
||||
|
||||
private constructor(protected repo: HelmRepo) {}
|
||||
protected readonly repo: SetRequired<HelmRepo, "cacheFilePath">;
|
||||
|
||||
private constructor(repo: HelmRepo) {
|
||||
assert(repo.cacheFilePath, "CacheFilePath must be provided on the helm repo");
|
||||
|
||||
this.repo = repo as SetRequired<HelmRepo, "cacheFilePath">;
|
||||
}
|
||||
|
||||
static forRepo(repo: HelmRepo) {
|
||||
return new this(repo);
|
||||
@@ -76,25 +84,30 @@ export class HelmChartManager {
|
||||
|
||||
const normalized = normalizeHelmCharts(this.repo.name, data.entries);
|
||||
|
||||
HelmChartManager.#cache.set(this.repo.name, {
|
||||
data: v8.serialize(normalized),
|
||||
mtimeMs: cacheFileStats.mtimeMs,
|
||||
});
|
||||
return put(
|
||||
HelmChartManager.#cache,
|
||||
this.repo.name,
|
||||
{
|
||||
data: v8.serialize(normalized),
|
||||
mtimeMs: cacheFileStats.mtimeMs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected async cachedYaml(): Promise<RepoHelmChartList> {
|
||||
if (!HelmChartManager.#cache.has(this.repo.name)) {
|
||||
await this.updateYamlCache();
|
||||
let cacheEntry = HelmChartManager.#cache.get(this.repo.name);
|
||||
|
||||
if (!cacheEntry) {
|
||||
cacheEntry = await this.updateYamlCache();
|
||||
} else {
|
||||
const newStats = await fs.promises.stat(this.repo.cacheFilePath);
|
||||
const cacheEntry = HelmChartManager.#cache.get(this.repo.name);
|
||||
|
||||
if (cacheEntry.mtimeMs < newStats.mtimeMs) {
|
||||
await this.updateYamlCache();
|
||||
cacheEntry = await this.updateYamlCache();
|
||||
}
|
||||
}
|
||||
|
||||
return v8.deserialize(HelmChartManager.#cache.get(this.repo.name).data);
|
||||
return v8.deserialize(cacheEntry.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import * as yaml from "js-yaml";
|
||||
import { toCamelCase } from "../../common/utils/camelCase";
|
||||
import { execFile } from "child_process";
|
||||
import { execHelm } from "./exec";
|
||||
import assert from "assert";
|
||||
import type { JsonObject, JsonValue } from "type-fest";
|
||||
import { isObject, json } from "../../common/utils";
|
||||
|
||||
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
|
||||
const args = [
|
||||
@@ -25,17 +28,17 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
|
||||
|
||||
args.push("--kubeconfig", pathToKubeconfig);
|
||||
|
||||
const output = JSON.parse(await execHelm(args));
|
||||
const output = json.parse(await execHelm(args));
|
||||
|
||||
if (!Array.isArray(output) || output.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return output.map(toCamelCase);
|
||||
return output.filter(isObject).map(toCamelCase);
|
||||
}
|
||||
|
||||
|
||||
export async function installChart(chart: string, values: any, name: string | undefined = "", namespace: string, version: string, kubeconfigPath: string) {
|
||||
export async function installChart(chart: string, values: JsonValue, name: string | undefined = "", namespace: string, version: string, kubeconfigPath: string) {
|
||||
const valuesFilePath = tempy.file({ name: "values.yaml" });
|
||||
|
||||
await fse.writeFile(valuesFilePath, yaml.dump(values));
|
||||
@@ -110,10 +113,14 @@ export async function getRelease(name: string, namespace: string, kubeconfigPath
|
||||
"--output", "json",
|
||||
];
|
||||
|
||||
const release = JSON.parse(await execHelm(args, {
|
||||
const release = json.parse(await execHelm(args, {
|
||||
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
||||
}));
|
||||
|
||||
if (!isObject(release) || Array.isArray(release)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
release.resources = await getResources(name, namespace, kubeconfigPath, kubectlPath);
|
||||
|
||||
return release;
|
||||
@@ -155,7 +162,7 @@ export async function getValues(name: string, { namespace, all = false, kubeconf
|
||||
}
|
||||
|
||||
export async function getHistory(name: string, namespace: string, kubeconfigPath: string) {
|
||||
return JSON.parse(await execHelm([
|
||||
return json.parse(await execHelm([
|
||||
"history",
|
||||
name,
|
||||
"--output", "json",
|
||||
@@ -193,7 +200,7 @@ async function getResources(name: string, namespace: string, kubeconfigPath: str
|
||||
try {
|
||||
const helmOutput = await execHelm(helmArgs);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<JsonObject[]>((resolve, reject) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const kubectl = execFile(kubectlPath, kubectlArgs);
|
||||
@@ -202,7 +209,9 @@ async function getResources(name: string, namespace: string, kubeconfigPath: str
|
||||
.on("exit", (code, signal) => {
|
||||
if (typeof code === "number") {
|
||||
if (code === 0) {
|
||||
resolve(JSON.parse(stdout).items);
|
||||
const output = json.parse(stdout) as { items: JsonObject[] };
|
||||
|
||||
resolve(output.items);
|
||||
} else {
|
||||
reject(stderr);
|
||||
}
|
||||
@@ -212,10 +221,11 @@ async function getResources(name: string, namespace: string, kubeconfigPath: str
|
||||
})
|
||||
.on("error", reject);
|
||||
|
||||
assert(kubectl.stderr && kubectl.stdout && kubectl.stdin, "For some reason the IO streams are undefined");
|
||||
|
||||
kubectl.stderr.on("data", output => stderr += output);
|
||||
kubectl.stdout.on("data", output => stdout += output);
|
||||
kubectl.stdin.write(helmOutput);
|
||||
kubectl.stdin.end();
|
||||
kubectl.stdin.end(helmOutput);
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
|
||||
@@ -11,9 +11,9 @@ import orderBy from "lodash/orderBy";
|
||||
import logger from "../logger";
|
||||
import { execHelm } from "./exec";
|
||||
|
||||
export type HelmEnv = Record<string, string> & {
|
||||
HELM_REPOSITORY_CACHE?: string;
|
||||
HELM_REPOSITORY_CONFIG?: string;
|
||||
export type HelmEnv = Partial<Record<string, string>> & {
|
||||
HELM_REPOSITORY_CACHE: string;
|
||||
HELM_REPOSITORY_CONFIG: string;
|
||||
};
|
||||
|
||||
export interface HelmRepoConfig {
|
||||
@@ -32,10 +32,14 @@ export interface HelmRepo {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface EnsuredHelmRepoManagerData {
|
||||
helmEnv: HelmEnv;
|
||||
didUpdateOnce: boolean;
|
||||
}
|
||||
|
||||
export class HelmRepoManager extends Singleton {
|
||||
protected repos: HelmRepo[];
|
||||
protected helmEnv: HelmEnv;
|
||||
protected didUpdateOnce: boolean;
|
||||
protected helmEnv?: HelmEnv;
|
||||
protected didUpdateOnce?: boolean;
|
||||
|
||||
public async loadAvailableRepos(): Promise<HelmRepo[]> {
|
||||
const res = await customRequestPromise({
|
||||
@@ -48,10 +52,10 @@ export class HelmRepoManager extends Singleton {
|
||||
return orderBy(res.body as HelmRepo[], repo => repo.name);
|
||||
}
|
||||
|
||||
private async ensureInitialized() {
|
||||
private async ensureInitialized(): Promise<EnsuredHelmRepoManagerData> {
|
||||
this.helmEnv ??= await this.parseHelmEnv();
|
||||
|
||||
const repos = await this.list();
|
||||
const repos = await this.list(this.helmEnv);
|
||||
|
||||
if (repos.length === 0) {
|
||||
await this.addRepo({
|
||||
@@ -64,12 +68,17 @@ export class HelmRepoManager extends Singleton {
|
||||
await this.update();
|
||||
this.didUpdateOnce = true;
|
||||
}
|
||||
|
||||
return {
|
||||
didUpdateOnce: this.didUpdateOnce,
|
||||
helmEnv: this.helmEnv,
|
||||
};
|
||||
}
|
||||
|
||||
protected async parseHelmEnv() {
|
||||
const output = await execHelm(["env"]);
|
||||
const lines = output.split(/\r?\n/); // split by new line feed
|
||||
const env: HelmEnv = {};
|
||||
const env: Partial<Record<string, string>> = {};
|
||||
|
||||
lines.forEach((line: string) => {
|
||||
const [key, value] = line.split("=");
|
||||
@@ -79,18 +88,18 @@ export class HelmRepoManager extends Singleton {
|
||||
}
|
||||
});
|
||||
|
||||
return env;
|
||||
return env as HelmEnv;
|
||||
}
|
||||
|
||||
public async repo(name: string): Promise<HelmRepo> {
|
||||
public async repo(name: string): Promise<HelmRepo | undefined> {
|
||||
const repos = await this.repositories();
|
||||
|
||||
return repos.find(repo => repo.name === name);
|
||||
}
|
||||
|
||||
private async list(): Promise<HelmRepo[]> {
|
||||
private async list(helmEnv: HelmEnv): Promise<HelmRepo[]> {
|
||||
try {
|
||||
const rawConfig = await readFile(this.helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
|
||||
const rawConfig = await readFile(helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
|
||||
const parsedConfig = yaml.load(rawConfig) as HelmRepoConfig;
|
||||
|
||||
if (typeof parsedConfig === "object" && parsedConfig) {
|
||||
@@ -105,13 +114,13 @@ export class HelmRepoManager extends Singleton {
|
||||
|
||||
public async repositories(): Promise<HelmRepo[]> {
|
||||
try {
|
||||
await this.ensureInitialized();
|
||||
const { helmEnv } = await this.ensureInitialized();
|
||||
|
||||
const repos = await this.list();
|
||||
const repos = await this.list(helmEnv);
|
||||
|
||||
return repos.map(repo => ({
|
||||
...repo,
|
||||
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
|
||||
cacheFilePath: `${helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(`[HELM]: repositories listing error`, error);
|
||||
|
||||
@@ -8,6 +8,8 @@ import logger from "../logger";
|
||||
import { HelmRepoManager } from "./helm-repo-manager";
|
||||
import { HelmChartManager } from "./helm-chart-manager";
|
||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||
import type { JsonObject } from "type-fest";
|
||||
import { object } from "../../common/utils";
|
||||
|
||||
interface GetReleaseValuesArgs {
|
||||
cluster: Cluster;
|
||||
@@ -15,8 +17,22 @@ interface GetReleaseValuesArgs {
|
||||
all: boolean;
|
||||
}
|
||||
|
||||
export interface InstallChartArgs {
|
||||
chart: string;
|
||||
values: JsonObject;
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface UpdateChartArgs {
|
||||
chart: string;
|
||||
values: JsonObject;
|
||||
version: string;
|
||||
}
|
||||
|
||||
class HelmService {
|
||||
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
||||
public async installChart(cluster: Cluster, data: InstallChartArgs) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
return installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
|
||||
@@ -25,13 +41,18 @@ class HelmService {
|
||||
public async listCharts() {
|
||||
const repositories = await HelmRepoManager.getInstance().repositories();
|
||||
|
||||
return Object.fromEntries(
|
||||
await Promise.all(repositories.map(async repo => [repo.name, await HelmChartManager.forRepo(repo).charts()])),
|
||||
return object.fromEntries(
|
||||
await Promise.all(repositories.map(async repo => [repo.name, await HelmChartManager.forRepo(repo).charts()] as const)),
|
||||
);
|
||||
}
|
||||
|
||||
public async getChart(repoName: string, chartName: string, version = "") {
|
||||
const repo = await HelmRepoManager.getInstance().repo(repoName);
|
||||
|
||||
if (!repo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chartManager = HelmChartManager.forRepo(repo);
|
||||
|
||||
return {
|
||||
@@ -43,10 +64,14 @@ class HelmService {
|
||||
public async getChartValues(repoName: string, chartName: string, version = "") {
|
||||
const repo = await HelmRepoManager.getInstance().repo(repoName);
|
||||
|
||||
if (!repo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return HelmChartManager.forRepo(repo).getValues(chartName, version);
|
||||
}
|
||||
|
||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||
public async listReleases(cluster: Cluster, namespace?: string) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("list releases");
|
||||
@@ -88,7 +113,7 @@ class HelmService {
|
||||
return deleteRelease(releaseName, namespace, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
|
||||
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const kubectl = await cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
|
||||
+8
-4
@@ -48,17 +48,18 @@ import extensionDiscoveryInjectable from "../extensions/extension-discovery/exte
|
||||
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 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 kubeconfigSyncManagerInjectable from "./catalog-sources/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";
|
||||
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
|
||||
import { broadcastNativeThemeOnUpdate } from "./native-theme";
|
||||
import assert from "assert";
|
||||
import windowManagerInjectable from "./window-manager.injectable";
|
||||
import navigateToPreferencesInjectable from "../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
|
||||
import syncGeneralCatalogEntitiesInjectable from "./catalog-sources/sync-general-catalog-entities.injectable";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
import hotbarStoreInjectable from "../common/hotbars/store.injectable";
|
||||
import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { init } from "@sentry/electron/main";
|
||||
@@ -70,6 +71,7 @@ async function main(di: DiContainer) {
|
||||
* Note: this MUST be called before electron's "ready" event has been emitted.
|
||||
*/
|
||||
initializeSentryReporting(init);
|
||||
|
||||
await di.runSetups();
|
||||
await app.whenReady();
|
||||
|
||||
@@ -249,11 +251,13 @@ async function main(di: DiContainer) {
|
||||
logger.info("🔌 Starting LensProxy");
|
||||
await lensProxy.listen(); // lensProxy.port available
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error ? String(error) : "unknown error"}`);
|
||||
|
||||
return app.exit();
|
||||
}
|
||||
|
||||
assert(lensProxy.port, "Lens Proxy failed to start");
|
||||
|
||||
// test proxy connection
|
||||
try {
|
||||
logger.info("🔎 Testing LensProxy connection ...");
|
||||
@@ -350,7 +354,7 @@ async function main(di: DiContainer) {
|
||||
|
||||
extensionLoader.initExtensions(extensions);
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error ? `: ${String(error)}` : ""}`);
|
||||
console.error(error);
|
||||
console.trace();
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
|
||||
import type { OpenDialogOptions } from "electron";
|
||||
import { BrowserWindow, dialog } from "electron";
|
||||
import { dialog } from "electron";
|
||||
import { WindowManager } from "../window-manager";
|
||||
|
||||
export async function showOpenDialog(dialogOptions: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[] }> {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOptions);
|
||||
const window = await WindowManager.getInstance().ensureMainWindow();
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(window, dialogOptions);
|
||||
|
||||
return { canceled, filePaths };
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { getBinaryName } from "../../common/vars";
|
||||
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
||||
import spawnInjectable from "../child-process/spawn.injectable";
|
||||
import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
|
||||
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
||||
|
||||
@@ -27,6 +28,7 @@ const createKubeAuthProxyInjectable = getInjectable({
|
||||
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
|
||||
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
|
||||
spawn: di.inject(spawnInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
};
|
||||
|
||||
return new KubeAuthProxy(dependencies, cluster, environmentVariables);
|
||||
|
||||
@@ -4,36 +4,32 @@
|
||||
*/
|
||||
|
||||
import type * as selfsigned from "selfsigned";
|
||||
import { getOrInsertWith } from "../../common/utils";
|
||||
|
||||
type SelfSignedGenerate = typeof selfsigned.generate;
|
||||
|
||||
const certCache: Map<string, selfsigned.SelfSignedCert> = new Map();
|
||||
const certCache = new Map<string, selfsigned.SelfSignedCert>();
|
||||
|
||||
export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSignedGenerate, useCache = true): selfsigned.SelfSignedCert {
|
||||
if (useCache && certCache.has(hostname)) {
|
||||
return certCache.get(hostname);
|
||||
}
|
||||
|
||||
const opts = [
|
||||
{ name: "commonName", value: "Lens Certificate Authority" },
|
||||
{ name: "organizationName", value: "Lens" },
|
||||
];
|
||||
|
||||
const cert = generate(opts, {
|
||||
keySize: 2048,
|
||||
algorithm: "sha256",
|
||||
days: 365,
|
||||
extensions: [
|
||||
{ name: "basicConstraints", cA: true },
|
||||
{ name: "subjectAltName", altNames: [
|
||||
{ type: 2, value: hostname },
|
||||
{ type: 2, value: "localhost" },
|
||||
{ type: 7, ip: "127.0.0.1" },
|
||||
] },
|
||||
export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSignedGenerate): selfsigned.SelfSignedCert {
|
||||
return getOrInsertWith(certCache, hostname, () => generate(
|
||||
[
|
||||
{ name: "commonName", value: "Lens Certificate Authority" },
|
||||
{ name: "organizationName", value: "Lens" },
|
||||
],
|
||||
});
|
||||
|
||||
certCache.set(hostname, cert);
|
||||
|
||||
return cert;
|
||||
{
|
||||
keySize: 2048,
|
||||
algorithm: "sha256",
|
||||
days: 365,
|
||||
extensions: [
|
||||
{ name: "basicConstraints", cA: true },
|
||||
{
|
||||
name: "subjectAltName", altNames: [
|
||||
{ type: 2, value: hostname },
|
||||
{ type: 2, value: "localhost" },
|
||||
{ type: 7, ip: "127.0.0.1" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ChildProcess, spawn } from "child_process";
|
||||
import type { ChildProcess } from "child_process";
|
||||
import { waitUntilUsed } from "tcp-port-used";
|
||||
import { randomBytes } from "crypto";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
@@ -11,27 +11,36 @@ import logger from "../logger";
|
||||
import { getPortFrom } from "../utils/get-port";
|
||||
import { makeObservable, observable, when } from "mobx";
|
||||
import type { SelfSignedCert } from "selfsigned";
|
||||
import assert from "assert";
|
||||
import { TypedRegEx } from "typed-regex";
|
||||
import type { Spawn } from "../child-process/spawn.injectable";
|
||||
import type { Logger } from "../../common/logger";
|
||||
|
||||
const startingServeRegex = /starting to serve on (?<address>.+)/i;
|
||||
const startingServeRegex = TypedRegEx("starting to serve on (?<address>.+)", "i");
|
||||
|
||||
export interface KubeAuthProxyDependencies {
|
||||
proxyBinPath: string;
|
||||
proxyCert: SelfSignedCert;
|
||||
spawn: typeof spawn;
|
||||
readonly proxyBinPath: string;
|
||||
readonly proxyCert: SelfSignedCert;
|
||||
spawn: Spawn;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class KubeAuthProxy {
|
||||
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
|
||||
|
||||
public get port(): number {
|
||||
return this._port;
|
||||
const port = this._port;
|
||||
|
||||
assert(port, "port has not yet been initialized");
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
protected _port: number;
|
||||
protected _port?: number;
|
||||
protected proxyProcess?: ChildProcess;
|
||||
@observable protected ready = false;
|
||||
|
||||
constructor(private dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
|
||||
constructor(private readonly dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@@ -63,7 +72,7 @@ export class KubeAuthProxy {
|
||||
});
|
||||
|
||||
this.proxyProcess.on("exit", (code) => {
|
||||
this.cluster.broadcastConnectUpdate(`proxy exited with code: ${code}`, code > 0);
|
||||
this.cluster.broadcastConnectUpdate(`proxy exited with code: ${code}`, code ? code > 0: false);
|
||||
this.exit();
|
||||
});
|
||||
|
||||
@@ -72,6 +81,9 @@ export class KubeAuthProxy {
|
||||
this.exit();
|
||||
});
|
||||
|
||||
assert(this.proxyProcess.stderr);
|
||||
assert(this.proxyProcess.stdout);
|
||||
|
||||
this.proxyProcess.stderr.on("data", (data: Buffer) => {
|
||||
if (data.includes("http: TLS handshake error")) {
|
||||
return;
|
||||
@@ -91,10 +103,13 @@ export class KubeAuthProxy {
|
||||
onFind: () => this.cluster.broadcastConnectUpdate("Authentication proxy started"),
|
||||
});
|
||||
|
||||
logger.info(`[KUBE-AUTH-PROXY]: found port=${this._port}`);
|
||||
|
||||
try {
|
||||
await waitUntilUsed(this.port, 500, 10000);
|
||||
this.ready = true;
|
||||
} catch (error) {
|
||||
logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error);
|
||||
this.cluster.broadcastConnectUpdate("Proxy port failed to be used within timelimit, restarting...", true);
|
||||
this.exit();
|
||||
|
||||
@@ -108,10 +123,10 @@ export class KubeAuthProxy {
|
||||
if (this.proxyProcess) {
|
||||
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
|
||||
this.proxyProcess.removeAllListeners();
|
||||
this.proxyProcess.stderr.removeAllListeners();
|
||||
this.proxyProcess.stdout.removeAllListeners();
|
||||
this.proxyProcess.stderr?.removeAllListeners();
|
||||
this.proxyProcess.stdout?.removeAllListeners();
|
||||
this.proxyProcess.kill();
|
||||
this.proxyProcess = null;
|
||||
this.proxyProcess = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,26 @@
|
||||
import { getInjectable } 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 type { KubeconfigManagerDependencies } from "./kubeconfig-manager";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
|
||||
export interface KubeConfigManagerInstantiationParameter {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export type CreateKubeconfigManager = (cluster: Cluster) => KubeconfigManager | undefined;
|
||||
|
||||
const createKubeconfigManagerInjectable = getInjectable({
|
||||
id: "create-kubeconfig-manager",
|
||||
|
||||
instantiate: (di) => {
|
||||
const dependencies = {
|
||||
instantiate: (di): CreateKubeconfigManager => {
|
||||
const dependencies: KubeconfigManagerDependencies = {
|
||||
directoryForTemp: di.inject(directoryForTempInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
};
|
||||
|
||||
return (cluster: Cluster) => new KubeconfigManager(dependencies, cluster);
|
||||
return (cluster) => new KubeconfigManager(dependencies, cluster);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import type { ClusterContextHandler } 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 { isErrnoException } from "../../common/utils";
|
||||
import type { PartialDeep } from "type-fest";
|
||||
import type { Logger } from "../../common/logger";
|
||||
|
||||
interface Dependencies {
|
||||
directoryForTemp: string;
|
||||
export interface KubeconfigManagerDependencies {
|
||||
readonly directoryForTemp: string;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class KubeconfigManager {
|
||||
@@ -26,9 +30,9 @@ export class KubeconfigManager {
|
||||
*/
|
||||
protected tempFilePath: string | null | undefined = null;
|
||||
|
||||
protected contextHandler: ContextHandler;
|
||||
protected readonly contextHandler: ClusterContextHandler;
|
||||
|
||||
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
|
||||
constructor(private readonly dependencies: KubeconfigManagerDependencies, protected cluster: Cluster) {
|
||||
this.contextHandler = cluster.contextHandler;
|
||||
}
|
||||
|
||||
@@ -42,7 +46,7 @@ export class KubeconfigManager {
|
||||
}
|
||||
|
||||
if (this.tempFilePath === null || !(await fs.pathExists(this.tempFilePath))) {
|
||||
await this.ensureFile();
|
||||
return await this.ensureFile();
|
||||
}
|
||||
|
||||
return this.tempFilePath;
|
||||
@@ -61,7 +65,7 @@ export class KubeconfigManager {
|
||||
try {
|
||||
await fs.unlink(this.tempFilePath);
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
if (isErrnoException(error) && error.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
@@ -72,7 +76,8 @@ export class KubeconfigManager {
|
||||
protected async ensureFile() {
|
||||
try {
|
||||
await this.contextHandler.ensureServer();
|
||||
this.tempFilePath = await this.createProxyKubeconfig();
|
||||
|
||||
return this.tempFilePath = await this.createProxyKubeconfig();
|
||||
} catch (error) {
|
||||
throw Object.assign(new Error("Failed to creat temp config for auth-proxy"), { cause: error });
|
||||
}
|
||||
@@ -94,13 +99,12 @@ export class KubeconfigManager {
|
||||
`kubeconfig-${id}`,
|
||||
);
|
||||
const kubeConfig = await cluster.getKubeconfig();
|
||||
const proxyConfig: Partial<KubeConfig> = {
|
||||
const proxyConfig: PartialDeep<KubeConfig> = {
|
||||
currentContext: contextName,
|
||||
clusters: [
|
||||
{
|
||||
name: contextName,
|
||||
server: this.resolveProxyUrl,
|
||||
skipTLSVerify: undefined,
|
||||
},
|
||||
],
|
||||
users: [
|
||||
@@ -111,7 +115,7 @@ export class KubeconfigManager {
|
||||
user: "proxy",
|
||||
name: contextName,
|
||||
cluster: contextName,
|
||||
namespace: cluster.defaultNamespace || kubeConfig.getContextObject(contextName).namespace,
|
||||
namespace: cluster.defaultNamespace || kubeConfig.getContextObject(contextName)?.namespace,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+12
-10
@@ -53,12 +53,11 @@ interface Dependencies {
|
||||
|
||||
export class Kubectl {
|
||||
public kubectlVersion: string;
|
||||
protected directory: string;
|
||||
protected url: string;
|
||||
protected path: string;
|
||||
protected dirname: string;
|
||||
|
||||
public static readonly bundledKubectlVersion: string = bundledVersion;
|
||||
public static readonly bundledKubectlVersion = bundledVersion;
|
||||
public static invalidBundle = false;
|
||||
|
||||
constructor(private dependencies: Dependencies, clusterVersion: string) {
|
||||
@@ -70,12 +69,14 @@ export class Kubectl {
|
||||
version = new SemVer(Kubectl.bundledKubectlVersion);
|
||||
}
|
||||
|
||||
const minorVersion = `${version.major}.${version.minor}`;
|
||||
const fromMajorMinor = kubectlMap.get(`${version.major}.${version.minor}`);
|
||||
|
||||
/* minorVersion is the first two digits of kube server version
|
||||
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
|
||||
if (kubectlMap.has(minorVersion)) {
|
||||
this.kubectlVersion = kubectlMap.get(minorVersion);
|
||||
/**
|
||||
* minorVersion is the first two digits of kube server version if the version map includes that,
|
||||
* use that version, if not, fallback to the exact x.y.z of kube version
|
||||
*/
|
||||
if (fromMajorMinor) {
|
||||
this.kubectlVersion = fromMajorMinor;
|
||||
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
|
||||
} else {
|
||||
this.kubectlVersion = version.format();
|
||||
@@ -176,8 +177,8 @@ export class Kubectl {
|
||||
return true;
|
||||
}
|
||||
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
|
||||
} catch (err) {
|
||||
logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`);
|
||||
} catch (error) {
|
||||
logger.error(`Local kubectl failed to run properly (${error}), unlinking`);
|
||||
}
|
||||
await fs.promises.unlink(this.path);
|
||||
}
|
||||
@@ -348,7 +349,8 @@ export class Kubectl {
|
||||
// MacOS packages are only available from default
|
||||
|
||||
const { url } = packageMirrors.get(this.dependencies.userStore.downloadMirror)
|
||||
?? packageMirrors.get(defaultPackageMirror);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
?? packageMirrors.get(defaultPackageMirror)!;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
+18
-12
@@ -9,15 +9,19 @@ import spdy from "spdy";
|
||||
import type httpProxy from "http-proxy";
|
||||
import { apiPrefix, apiKubePrefix } from "../common/vars";
|
||||
import type { Router } from "./router/router";
|
||||
import type { ContextHandler } from "./context-handler/context-handler";
|
||||
import type { ClusterContextHandler } from "./context-handler/context-handler";
|
||||
import logger from "./logger";
|
||||
import { Singleton } from "../common/utils";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { getBoolean } from "./utils/parse-query";
|
||||
import assert from "assert";
|
||||
import type { SetRequired } from "type-fest";
|
||||
|
||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
|
||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
||||
|
||||
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
|
||||
|
||||
export interface LensProxyFunctions {
|
||||
getClusterForRequest: GetClusterForRequest;
|
||||
@@ -53,13 +57,12 @@ const disallowedPorts = new Set([
|
||||
]);
|
||||
|
||||
export class LensProxy extends Singleton {
|
||||
protected origin: string;
|
||||
protected proxyServer: http.Server;
|
||||
protected closed = false;
|
||||
protected retryCounters = new Map<string, number>();
|
||||
protected getClusterForRequest: GetClusterForRequest;
|
||||
|
||||
public port: number;
|
||||
public port?: number;
|
||||
|
||||
constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiUpgradeRequest, getClusterForRequest }: LensProxyFunctions) {
|
||||
super();
|
||||
@@ -73,12 +76,12 @@ export class LensProxy extends Singleton {
|
||||
plain: true,
|
||||
protocols: ["http/1.1", "spdy/3.1"],
|
||||
},
|
||||
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(req, res);
|
||||
}, (req, res) => {
|
||||
this.handleRequest(req as ServerIncomingMessage, res);
|
||||
});
|
||||
|
||||
this.proxyServer
|
||||
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||
.on("upgrade", (req: ServerIncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||
const cluster = getClusterForRequest(req);
|
||||
|
||||
if (!cluster) {
|
||||
@@ -197,7 +200,7 @@ export class LensProxy extends Singleton {
|
||||
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
||||
setTimeout(() => {
|
||||
this.retryCounters.set(reqId, retryCount + 1);
|
||||
this.handleRequest(req, res)
|
||||
this.handleRequest(req as ServerIncomingMessage, res)
|
||||
.catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`));
|
||||
}, timeoutMs);
|
||||
}
|
||||
@@ -214,8 +217,8 @@ export class LensProxy extends Singleton {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions | void> {
|
||||
if (req.url.startsWith(apiKubePrefix)) {
|
||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise<httpProxy.ServerOptions | void> {
|
||||
if (req.url?.startsWith(apiKubePrefix)) {
|
||||
delete req.headers.authorization;
|
||||
req.url = req.url.replace(apiKubePrefix, "");
|
||||
|
||||
@@ -223,11 +226,13 @@ export class LensProxy extends Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
protected getRequestId(req: http.IncomingMessage) {
|
||||
protected getRequestId(req: http.IncomingMessage): string {
|
||||
assert(req.headers.host);
|
||||
|
||||
return req.headers.host + req.url;
|
||||
}
|
||||
|
||||
protected async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) {
|
||||
const cluster = this.getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
@@ -237,6 +242,7 @@ export class LensProxy extends Singleton {
|
||||
return this.proxy.web(req, res, proxyTarget);
|
||||
}
|
||||
}
|
||||
|
||||
this.router.route(cluster, req, res);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,7 @@ import { openBrowser } from "../../common/utils";
|
||||
import { showAbout } from "./menu";
|
||||
import windowManagerInjectable from "../window-manager.injectable";
|
||||
import type { MenuItemConstructorOptions } from "electron";
|
||||
import {
|
||||
webContents,
|
||||
} from "electron";
|
||||
import { webContents } from "electron";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import appNameInjectable from "../app-paths/app-name/app-name.injectable";
|
||||
import electronMenuItemsInjectable from "./electron-menu-items.injectable";
|
||||
@@ -26,7 +24,7 @@ import navigateToAddClusterInjectable from "../../common/front-end-routing/route
|
||||
import isMacInjectable from "../../common/vars/is-mac.injectable";
|
||||
import { computed } from "mobx";
|
||||
|
||||
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
|
||||
function ignoreIf(check: boolean, menuItems: MenuItemOpts[]) {
|
||||
return check ? [] : menuItems;
|
||||
}
|
||||
|
||||
@@ -142,19 +140,14 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
},
|
||||
},
|
||||
]),
|
||||
|
||||
{ type: "separator" },
|
||||
|
||||
...(isMac
|
||||
? ([
|
||||
{
|
||||
role: "close",
|
||||
label: "Close Window",
|
||||
accelerator: "Shift+Cmd+W",
|
||||
},
|
||||
] as MenuItemConstructorOptions[])
|
||||
: []),
|
||||
|
||||
...ignoreIf(!isMac, [
|
||||
{
|
||||
role: "close",
|
||||
label: "Close Window",
|
||||
accelerator: "Shift+Cmd+W",
|
||||
},
|
||||
]),
|
||||
...ignoreIf(isMac, [
|
||||
{
|
||||
label: "Exit",
|
||||
@@ -311,7 +304,9 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
|
||||
// Modify menu from extensions-api
|
||||
for (const menuItem of electronMenuItems.get()) {
|
||||
if (!appMenu.has(menuItem.parentId)) {
|
||||
const parentMenu = appMenu.get(menuItem.parentId);
|
||||
|
||||
if (!parentMenu) {
|
||||
logger.error(
|
||||
`[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
|
||||
{ menuItem },
|
||||
@@ -320,7 +315,7 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
continue;
|
||||
}
|
||||
|
||||
appMenu.get(menuItem.parentId).submenu.push(menuItem);
|
||||
(parentMenu.submenu ??= []).push(menuItem);
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
|
||||
@@ -37,9 +37,7 @@ export function showAbout() {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildMenu(
|
||||
applicationMenuItems: MenuItemOpts[],
|
||||
) {
|
||||
export function buildMenu(applicationMenuItems: MenuItemOpts[]) {
|
||||
Menu.setApplicationMenu(
|
||||
Menu.buildFromTemplate(applicationMenuItems),
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ const navigateToUrlInjectable = getInjectable({
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
|
||||
return (url) => {
|
||||
windowManager.navigateSync(url);
|
||||
windowManager.navigate(url);
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||
import { inspect } from "util";
|
||||
import { Singleton } from "../../common/utils";
|
||||
import { isRequestError, Singleton } from "../../common/utils";
|
||||
|
||||
export interface PrometheusService {
|
||||
id: string;
|
||||
@@ -30,9 +29,9 @@ export abstract class PrometheusProvider {
|
||||
protected async getFirstNamespacedService(client: CoreV1Api, ...selectors: string[]): Promise<PrometheusService> {
|
||||
try {
|
||||
for (const selector of selectors) {
|
||||
const { body: { items: [service] }} = await client.listServiceForAllNamespaces(null, null, null, selector);
|
||||
const { body: { items: [service] }} = await client.listServiceForAllNamespaces(undefined, undefined, undefined, selector);
|
||||
|
||||
if (service) {
|
||||
if (service?.metadata?.namespace && service.metadata.name && service.spec?.ports) {
|
||||
return {
|
||||
id: this.id,
|
||||
namespace: service.metadata.namespace,
|
||||
@@ -42,7 +41,7 @@ export abstract class PrometheusProvider {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list services for Prometheus${this.name} in all namespaces: ${error.response.body.message}`);
|
||||
throw new Error(`Failed to list services for Prometheus${this.name} in all namespaces: ${isRequestError(error) ? error.response?.body.message : error}`);
|
||||
}
|
||||
|
||||
throw new Error(`No service found for Prometheus${this.name} from any namespace`);
|
||||
@@ -50,8 +49,11 @@ export abstract class PrometheusProvider {
|
||||
|
||||
protected async getNamespacedService(client: CoreV1Api, name: string, namespace: string): Promise<PrometheusService> {
|
||||
try {
|
||||
const resp = await client.readNamespacedService(name, namespace);
|
||||
const service = resp.body;
|
||||
const { body: service } = await client.readNamespacedService(name, namespace);
|
||||
|
||||
if (!service.metadata?.namespace || !service.metadata.name || !service.spec?.ports) {
|
||||
throw new Error(`Service returned from Prometheus${this.name} in namespace="${namespace}" did not have required information`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
@@ -60,7 +62,7 @@ export abstract class PrometheusProvider {
|
||||
port: service.spec.ports[0].port,
|
||||
};
|
||||
} catch(error) {
|
||||
throw new Error(`Failed to list services for Prometheus${this.name} in namespace=${inspect(namespace, false, undefined, false)}: ${error.response.body.message}`);
|
||||
throw new Error(`Failed to list services for Prometheus${this.name} in namespace="${namespace}": ${isRequestError(error) ? error.response?.body.message : error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,18 @@ import * as uuid from "uuid";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
||||
import { delay, noop } from "../../../common/utils";
|
||||
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 { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||
import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
|
||||
import { LensExtension } from "../../../extensions/lens-extension";
|
||||
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||
import type { ObservableMap } from "mobx";
|
||||
import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable";
|
||||
|
||||
jest.mock("../../../common/ipc");
|
||||
|
||||
@@ -28,9 +30,7 @@ function throwIfDefined(val: any): void {
|
||||
}
|
||||
|
||||
describe("protocol router tests", () => {
|
||||
// TODO: This test suite is using any to access protected property.
|
||||
// Unit tests are allowed to only public interfaces.
|
||||
let extensionLoader: any;
|
||||
let extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
|
||||
let lpr: LensProtocolRouterMain;
|
||||
let extensionsStore: ExtensionsStore;
|
||||
|
||||
@@ -48,10 +48,8 @@ describe("protocol router tests", () => {
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
extensionInstances = di.inject(extensionInstancesInjectable);
|
||||
extensionsStore = di.inject(extensionsStoreInjectable);
|
||||
|
||||
lpr = di.inject(lensProtocolRouterMainInjectable);
|
||||
|
||||
lpr.rendererLoaded = true;
|
||||
@@ -100,8 +98,8 @@ describe("protocol router tests", () => {
|
||||
handler: noop,
|
||||
});
|
||||
|
||||
extensionLoader.instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
||||
extensionInstances.set(extId, ext);
|
||||
extensionsStore.mergeState([[extId, { enabled: true, name: "@mirantis/minikube" }]]);
|
||||
|
||||
lpr.addInternalHandler("/", noop);
|
||||
|
||||
@@ -180,8 +178,8 @@ describe("protocol router tests", () => {
|
||||
handler: params => { called = params.pathname.id; },
|
||||
});
|
||||
|
||||
extensionLoader.instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
extensionInstances.set(extId, ext);
|
||||
extensionsStore.mergeState([[extId, { enabled: true, name: "@foobar/icecream" }]]);
|
||||
|
||||
try {
|
||||
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||
@@ -219,8 +217,8 @@ describe("protocol router tests", () => {
|
||||
handler: params => { called = params.pathname.id; },
|
||||
});
|
||||
|
||||
extensionLoader.instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
extensionInstances.set(extId, ext);
|
||||
extensionsStore.mergeState([[extId, { enabled: true, name: "@foobar/icecream" }]]);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -245,12 +243,14 @@ describe("protocol router tests", () => {
|
||||
handler: () => { called = 1; },
|
||||
});
|
||||
|
||||
extensionLoader.instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" });
|
||||
extensionInstances.set(extId, ext);
|
||||
extensionsStore.mergeState([[extId, { enabled: true, name: "icecream" }]]);
|
||||
}
|
||||
|
||||
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||
(extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||
extensionsStore.mergeState([
|
||||
["@foobar/icecream", { enabled: true, name: "@foobar/icecream" }],
|
||||
["icecream", { enabled: true, name: "icecream" }],
|
||||
]);
|
||||
|
||||
try {
|
||||
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||
|
||||
@@ -84,7 +84,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
await this._routeToExtension(url);
|
||||
}
|
||||
} catch (error) {
|
||||
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
||||
broadcastMessage(ProtocolHandlerInvalid, error ? String(error) : "unknown error", rawUrl);
|
||||
|
||||
if (error instanceof proto.RoutingError) {
|
||||
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
|
||||
@@ -118,7 +118,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected _routeToInternal(url: URLParse<Record<string, string>>): RouteAttempt {
|
||||
protected _routeToInternal(url: URLParse<Record<string, string | undefined>>): RouteAttempt {
|
||||
const rawUrl = url.toString(); // for sending to renderer
|
||||
const attempt = super._routeToInternal(url);
|
||||
|
||||
@@ -127,7 +127,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
return attempt;
|
||||
}
|
||||
|
||||
protected async _routeToExtension(url: URLParse<Record<string, string>>): Promise<RouteAttempt> {
|
||||
protected async _routeToExtension(url: URLParse<Record<string, string | undefined>>): Promise<RouteAttempt> {
|
||||
const rawUrl = url.toString(); // for sending to renderer
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { chunk } from "lodash";
|
||||
import tls from "tls";
|
||||
import type { ConnectionOptions } from "tls";
|
||||
import { connect } from "tls";
|
||||
import url from "url";
|
||||
import { apiKubePrefix } from "../../common/vars";
|
||||
import type { ProxyApiRequestArgs } from "./types";
|
||||
@@ -13,16 +14,16 @@ const skipRawHeaders = new Set(["Host", "Authorization"]);
|
||||
|
||||
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||
const proxyCa = await cluster.contextHandler.resolveAuthProxyCa();
|
||||
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
|
||||
const apiUrl = url.parse(cluster.apiUrl);
|
||||
const pUrl = url.parse(proxyUrl);
|
||||
const connectOpts = {
|
||||
port: parseInt(pUrl.port),
|
||||
host: pUrl.hostname,
|
||||
const connectOpts: ConnectionOptions = {
|
||||
port: pUrl.port ? parseInt(pUrl.port) : undefined,
|
||||
host: pUrl.hostname ?? undefined,
|
||||
ca: proxyCa,
|
||||
};
|
||||
|
||||
const proxySocket = tls.connect(connectOpts, () => {
|
||||
const proxySocket = connect(connectOpts, () => {
|
||||
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
||||
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
|
||||
interface Dependencies {
|
||||
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string) => boolean;
|
||||
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
|
||||
|
||||
createShellSession: (args: {
|
||||
webSocket: WebSocket;
|
||||
@@ -27,7 +27,7 @@ export const shellApiRequest = ({ createShellSession, authenticateRequest }: Dep
|
||||
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)) {
|
||||
if (!tabId || !cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
|
||||
socket.write("Invalid shell request");
|
||||
|
||||
return void socket.end();
|
||||
|
||||
+2
-2
@@ -31,10 +31,10 @@ export class ShellRequestAuthenticator {
|
||||
* @param token The value that is being presented as a one time authentication token
|
||||
* @returns `true` if `token` was valid, false otherwise
|
||||
*/
|
||||
authenticate = (clusterId: ClusterId, tabId: string, token: string): boolean => {
|
||||
authenticate = (clusterId: ClusterId, tabId: string, token: string | undefined): boolean => {
|
||||
const clusterTokens = this.tokens.get(clusterId);
|
||||
|
||||
if (!clusterTokens) {
|
||||
if (!clusterTokens || !tabId || !token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import type http from "http";
|
||||
import type net from "net";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
|
||||
export interface ProxyApiRequestArgs {
|
||||
req: http.IncomingMessage;
|
||||
req: SetRequired<http.IncomingMessage, "url" | "method">;
|
||||
socket: net.Socket;
|
||||
head: Buffer;
|
||||
cluster: Cluster;
|
||||
|
||||
@@ -12,7 +12,7 @@ import path from "path";
|
||||
import tempy from "tempy";
|
||||
import logger from "./logger";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { cloneJsonObject } from "../common/utils";
|
||||
import { isChildProcessError } from "../common/utils";
|
||||
import type { Patch } from "rfc6902";
|
||||
import { promiseExecFile } from "../common/utils/promise-exec";
|
||||
|
||||
@@ -54,7 +54,11 @@ export class ResourceApplier {
|
||||
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
throw error.stderr ?? error;
|
||||
if (isChildProcessError(error)) {
|
||||
throw error.stderr ?? error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +96,11 @@ export class ResourceApplier {
|
||||
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
throw error?.stderr ?? error;
|
||||
if (isChildProcessError(error)) {
|
||||
throw error.stderr ?? error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await fs.unlink(fileName);
|
||||
}
|
||||
@@ -142,15 +150,16 @@ export class ResourceApplier {
|
||||
}
|
||||
|
||||
protected sanitizeObject(resource: KubernetesObject | any) {
|
||||
resource = cloneJsonObject(resource);
|
||||
delete resource.status;
|
||||
delete resource.metadata?.resourceVersion;
|
||||
const annotations = resource.metadata?.annotations;
|
||||
const res = JSON.parse(JSON.stringify(resource));
|
||||
|
||||
delete res.status;
|
||||
delete res.metadata?.resourceVersion;
|
||||
const annotations = res.metadata?.annotations;
|
||||
|
||||
if (annotations) {
|
||||
delete annotations["kubectl.kubernetes.io/last-applied-configuration"];
|
||||
}
|
||||
|
||||
return resource;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type http from "http";
|
||||
import type httpProxy from "http-proxy";
|
||||
import type { LensApiResultContentType } from "./router-content-types";
|
||||
import type { URLSearchParams } from "url";
|
||||
import type Joi from "joi";
|
||||
|
||||
export type InferParam<
|
||||
T extends string,
|
||||
PathParams extends Record<string, string>,
|
||||
> =
|
||||
T extends `{${infer P}?}`
|
||||
? PathParams & Partial<Record<P, string>>
|
||||
: T extends `{${infer P}}`
|
||||
? PathParams & Record<P, string>
|
||||
: PathParams;
|
||||
|
||||
export type InferParamFromPath<P extends string> =
|
||||
P extends `${string}/{${infer B}*}${infer Tail}`
|
||||
? Tail extends ""
|
||||
? Record<B, string>
|
||||
: never
|
||||
: P extends `${infer A}/${infer B}`
|
||||
? InferParam<A, InferParamFromPath<B>>
|
||||
: InferParam<P, {}>;
|
||||
|
||||
export interface LensApiRequest<Path extends string> {
|
||||
path: Path;
|
||||
payload: unknown;
|
||||
params: InferParamFromPath<Path>;
|
||||
cluster: Cluster | undefined;
|
||||
query: URLSearchParams;
|
||||
raw: {
|
||||
req: http.IncomingMessage;
|
||||
res: http.ServerResponse;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClusterLensApiRequest<Path extends string> extends LensApiRequest<Path> {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export interface LensApiResult<Response> {
|
||||
statusCode?: number;
|
||||
response?: Response;
|
||||
error?: any;
|
||||
contentType?: LensApiResultContentType;
|
||||
headers?: Partial<Record<string, string>>;
|
||||
proxy?: httpProxy;
|
||||
}
|
||||
|
||||
export type RouteResponse<Response> =
|
||||
| LensApiResult<Response>
|
||||
| void;
|
||||
|
||||
export interface RouteHandler<TResponse, Path extends string>{
|
||||
(request: LensApiRequest<Path>): RouteResponse<TResponse> | Promise<RouteResponse<TResponse>>;
|
||||
}
|
||||
|
||||
export interface BaseRoutePaths<Path extends string> {
|
||||
path: Path;
|
||||
method: "get" | "post" | "put" | "patch" | "delete";
|
||||
}
|
||||
|
||||
export interface PayloadValidator<Payload> {
|
||||
validate(payload: unknown): Joi.ValidationResult<Payload>;
|
||||
}
|
||||
|
||||
export interface ValidatorBaseRoutePaths<Path extends string, Payload> extends BaseRoutePaths<Path> {
|
||||
payloadValidator: PayloadValidator<Payload>;
|
||||
}
|
||||
|
||||
export interface Route<TResponse, Path extends string> extends BaseRoutePaths<Path> {
|
||||
handler: RouteHandler<TResponse, Path>;
|
||||
}
|
||||
|
||||
export interface BindHandler<Path extends string> {
|
||||
<TResponse>(handler: RouteHandler<TResponse, Path>): Route<TResponse, Path>;
|
||||
}
|
||||
|
||||
export function route<Path extends string>(parts: BaseRoutePaths<Path>): BindHandler<Path> {
|
||||
return (handler) => ({
|
||||
...parts,
|
||||
handler,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ClusterRouteHandler<Response, Path extends string>{
|
||||
(request: ClusterLensApiRequest<Path>): RouteResponse<Response> | Promise<RouteResponse<Response>>;
|
||||
}
|
||||
|
||||
export interface BindClusterHandler<Path extends string> {
|
||||
<TResponse>(handler: ClusterRouteHandler<TResponse, Path>): Route<TResponse, Path>;
|
||||
}
|
||||
|
||||
export function clusterRoute<Path extends string>(parts: BaseRoutePaths<Path>): BindClusterHandler<Path> {
|
||||
return (handler) => ({
|
||||
...parts,
|
||||
handler: ({ cluster, ...rest }) => {
|
||||
if (!cluster) {
|
||||
return {
|
||||
error: "Cluster missing",
|
||||
statusCode: 400,
|
||||
};
|
||||
}
|
||||
|
||||
return handler({ cluster, ...rest });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface ValidatedClusterLensApiRequest<Path extends string, Payload> extends ClusterLensApiRequest<Path> {
|
||||
payload: Payload;
|
||||
}
|
||||
|
||||
export interface ValidatedClusterRouteHandler<Payload, Response, Path extends string> {
|
||||
(request: ValidatedClusterLensApiRequest<Path, Payload>): RouteResponse<Response> | Promise<RouteResponse<Response>>;
|
||||
}
|
||||
|
||||
export interface BindValidatedClusterHandler<Path extends string, Payload> {
|
||||
<Response>(handler: ValidatedClusterRouteHandler<Payload, Response, Path>): Route<Response, Path>;
|
||||
}
|
||||
|
||||
export function payloadValidatedClusterRoute<Path extends string, Payload>({ payloadValidator, ...parts }: ValidatorBaseRoutePaths<Path, Payload>): BindValidatedClusterHandler<Path, Payload> {
|
||||
const boundClusterRoute = clusterRoute(parts);
|
||||
|
||||
return (handler) => boundClusterRoute(({ payload, ...rest }) => {
|
||||
const validationResult = payloadValidator.validate(payload);
|
||||
|
||||
if (validationResult.error) {
|
||||
return {
|
||||
error: validationResult.error,
|
||||
statusCode: 400,
|
||||
};
|
||||
}
|
||||
|
||||
return handler({
|
||||
payload: validationResult.value,
|
||||
...rest,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensApiResult } from "./router";
|
||||
import type { LensApiResult } from "./route";
|
||||
|
||||
export interface LensApiResultContentType {
|
||||
resultMapper: (result: LensApiResult<any>) => ({
|
||||
|
||||
@@ -2,15 +2,29 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { Route } from "./router";
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
import { getInjectable, getInjectionToken, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { Router } from "./router";
|
||||
import parseRequestInjectable from "./parse-request.injectable";
|
||||
import type { Route } from "./route";
|
||||
|
||||
export const routeInjectionToken = getInjectionToken<Route<any>>({
|
||||
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
|
||||
id: "route-injection-token",
|
||||
});
|
||||
|
||||
export function getRouteInjectable<
|
||||
T,
|
||||
Path extends string,
|
||||
>(
|
||||
opts: Omit<Injectable<Route<T, Path>, Route<T, Path>, void>, "lifecycle" | "injectionToken">,
|
||||
): Injectable<Route<T, Path>, Route<T, Path>, void> {
|
||||
return {
|
||||
...opts,
|
||||
injectionToken: routeInjectionToken as never,
|
||||
lifecycle: lifecycleEnum.singleton as never,
|
||||
};
|
||||
}
|
||||
|
||||
const routerInjectable = getInjectable({
|
||||
id: "router",
|
||||
|
||||
|
||||
@@ -5,22 +5,33 @@
|
||||
|
||||
import routerInjectable, { routeInjectionToken } from "./router.injectable";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { Router, RouteHandler, Route } from "./router";
|
||||
import type { Router } from "./router";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { Request } from "mock-http";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import parseRequestInjectable from "./parse-request.injectable";
|
||||
import { contentTypes } from "./router-content-types";
|
||||
import mockFs from "mock-fs";
|
||||
import type { MockInstance } from "jest-mock";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import type { Route, RouteHandler } from "./route";
|
||||
|
||||
type AsyncFnMock<
|
||||
TToBeMocked extends (...args: any[]) => any,
|
||||
TArguments extends Parameters<TToBeMocked> = Parameters<TToBeMocked>,
|
||||
TResolve extends Awaited<ReturnType<TToBeMocked>> = Awaited<ReturnType<TToBeMocked>>,
|
||||
> = MockInstance<(...args: TArguments) => Promise<TResolve>, TArguments> & {
|
||||
resolve: (resolvedValue: TResolve) => Promise<void>;
|
||||
reject: (rejectValue?: any) => Promise<void>;
|
||||
} & ((...args: TArguments) => Promise<TResolve>);
|
||||
|
||||
describe("router", () => {
|
||||
let router: Router;
|
||||
let routeHandlerMock: AsyncFnMock<RouteHandler<any>>;
|
||||
let routeHandlerMock: AsyncFnMock<RouteHandler<any, string>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
routeHandlerMock = asyncFn();
|
||||
routeHandlerMock = asyncFn() as AsyncFnMock<RouteHandler<any, string>>;
|
||||
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
@@ -37,7 +48,7 @@ describe("router", () => {
|
||||
method: "get",
|
||||
path: "/some-path",
|
||||
handler: routeHandlerMock,
|
||||
} as Route<any>),
|
||||
} as Route<any, string>),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
});
|
||||
@@ -54,7 +65,7 @@ describe("router", () => {
|
||||
describe("when navigating to the route", () => {
|
||||
let actualPromise: Promise<boolean>;
|
||||
let clusterStub: Cluster;
|
||||
let requestStub: Request;
|
||||
let requestStub: SetRequired<Request, "url" | "method">;
|
||||
let responseStub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -64,7 +75,7 @@ describe("router", () => {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
}) as SetRequired<Request, "url" | "method">;
|
||||
|
||||
responseStub = { end: jest.fn(), setHeader: jest.fn(), write: jest.fn(), statusCode: undefined };
|
||||
|
||||
|
||||
+10
-55
@@ -5,42 +5,20 @@
|
||||
|
||||
import Call from "@hapi/call";
|
||||
import type http from "http";
|
||||
import type httpProxy from "http-proxy";
|
||||
import { toPairs } from "lodash/fp";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { LensApiResultContentType } from "./router-content-types";
|
||||
import { contentTypes } from "./router-content-types";
|
||||
import type { ServerIncomingMessage } from "../lens-proxy";
|
||||
import type { LensApiRequest, LensApiResult, Route } from "./route";
|
||||
|
||||
export interface RouterRequestOpts {
|
||||
req: http.IncomingMessage;
|
||||
res: http.ServerResponse;
|
||||
cluster: Cluster;
|
||||
params: RouteParams;
|
||||
cluster: Cluster | undefined;
|
||||
params: any;
|
||||
url: URL;
|
||||
}
|
||||
|
||||
export interface RouteParams extends Record<string, string> {
|
||||
path?: string; // *-route
|
||||
namespace?: string;
|
||||
service?: string;
|
||||
account?: string;
|
||||
release?: string;
|
||||
repo?: string;
|
||||
chart?: string;
|
||||
}
|
||||
|
||||
export interface LensApiRequest<P = any> {
|
||||
path: string;
|
||||
payload: P;
|
||||
params: RouteParams;
|
||||
cluster: Cluster;
|
||||
query: URLSearchParams;
|
||||
raw: {
|
||||
req: http.IncomingMessage;
|
||||
res: http.ServerResponse;
|
||||
};
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
parseRequest: (request: http.IncomingMessage, _: null, options: { parse: boolean; output: string }) => Promise<{ payload: any }>;
|
||||
}
|
||||
@@ -48,13 +26,13 @@ interface Dependencies {
|
||||
export class Router {
|
||||
protected router = new Call.Router();
|
||||
|
||||
constructor(routes: Route<unknown>[], private dependencies: Dependencies) {
|
||||
constructor(routes: Route<unknown, string>[], private dependencies: Dependencies) {
|
||||
routes.forEach(route => {
|
||||
this.router.add({ method: route.method, path: route.path }, handleRoute(route));
|
||||
});
|
||||
}
|
||||
|
||||
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
||||
public async route(cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
const path = url.pathname;
|
||||
const method = req.method.toLowerCase();
|
||||
@@ -72,7 +50,7 @@ export class Router {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest> {
|
||||
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest<string>> {
|
||||
const { req, res, url, cluster, params } = opts;
|
||||
|
||||
const { payload } = await this.dependencies.parseRequest(req, null, {
|
||||
@@ -86,37 +64,14 @@ export class Router {
|
||||
raw: {
|
||||
req, res,
|
||||
},
|
||||
query: url.searchParams,
|
||||
query: url.searchParams as never,
|
||||
payload,
|
||||
params,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface LensApiResult<TResult> {
|
||||
statusCode?: number;
|
||||
response?: TResult;
|
||||
error?: any;
|
||||
contentType?: LensApiResultContentType;
|
||||
headers?: { [name: string]: string };
|
||||
proxy?: httpProxy;
|
||||
}
|
||||
|
||||
export type RouteHandler<TResponse> = (
|
||||
request: LensApiRequest
|
||||
) =>
|
||||
| Promise<LensApiResult<TResponse>>
|
||||
| Promise<void>
|
||||
| LensApiResult<TResponse>
|
||||
| void;
|
||||
|
||||
export interface Route<TResponse> {
|
||||
path: string;
|
||||
method: "get" | "post" | "put" | "patch" | "delete";
|
||||
handler: RouteHandler<TResponse>;
|
||||
}
|
||||
|
||||
const handleRoute = (route: Route<unknown>) => async (request: LensApiRequest, response: http.ServerResponse) => {
|
||||
const handleRoute = (route: Route<unknown, string>) => async (request: LensApiRequest<string>, response: http.ServerResponse) => {
|
||||
let result: LensApiResult<any> | void;
|
||||
|
||||
const writeServerResponse = writeServerResponseFor(response);
|
||||
@@ -126,7 +81,7 @@ const handleRoute = (route: Route<unknown>) => async (request: LensApiRequest, r
|
||||
} catch(error) {
|
||||
const mappedResult = contentTypes.txt.resultMapper({
|
||||
statusCode: 500,
|
||||
error: error.toString(),
|
||||
error: error ? String(error) : "unknown error",
|
||||
});
|
||||
|
||||
writeServerResponse(mappedResult);
|
||||
|
||||
@@ -2,35 +2,28 @@
|
||||
* 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 { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { RawHelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { route } from "../../../router/route";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
|
||||
interface GetChartResponse {
|
||||
readme: string;
|
||||
versions: RawHelmChart[];
|
||||
}
|
||||
|
||||
const getChartRouteInjectable = getInjectable({
|
||||
const getChartRouteInjectable = getRouteInjectable({
|
||||
id: "get-chart-route",
|
||||
|
||||
instantiate: (): Route<GetChartResponse> => ({
|
||||
instantiate: () => route({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/v2/charts/{repo}/{chart}`,
|
||||
})(async ({ params, query }) => {
|
||||
const { repo, chart } = params;
|
||||
|
||||
handler: async ({ params, query }) => ({
|
||||
return {
|
||||
response: await helmService.getChart(
|
||||
params.repo,
|
||||
params.chart,
|
||||
query.get("version"),
|
||||
repo,
|
||||
chart,
|
||||
query.get("version") ?? undefined,
|
||||
),
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
});
|
||||
|
||||
export default getChartRouteInjectable;
|
||||
@@ -2,32 +2,24 @@
|
||||
* 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 { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import { route } from "../../../router/route";
|
||||
|
||||
const getChartRouteValuesInjectable = getInjectable({
|
||||
const getChartRouteValuesInjectable = getRouteInjectable({
|
||||
id: "get-chart-route-values",
|
||||
|
||||
instantiate: (): Route<string> => ({
|
||||
instantiate: () => route({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/v2/charts/{repo}/{chart}/values`,
|
||||
|
||||
handler: async ({
|
||||
params,
|
||||
query,
|
||||
}) => ({
|
||||
response: await helmService.getChartValues(
|
||||
params.repo,
|
||||
params.chart,
|
||||
query.get("version"),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(async ({ params, query }) => ({
|
||||
response: await helmService.getChartValues(
|
||||
params.repo,
|
||||
params.chart,
|
||||
query.get("version") ?? undefined,
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
export default getChartRouteValuesInjectable;
|
||||
@@ -2,25 +2,20 @@
|
||||
* 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 { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import { route } from "../../../router/route";
|
||||
|
||||
const listChartsRouteInjectable = getInjectable({
|
||||
const listChartsRouteInjectable = getRouteInjectable({
|
||||
id: "list-charts-route",
|
||||
|
||||
instantiate: (): Route<any> => ({
|
||||
instantiate: () => route({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/v2/charts`,
|
||||
|
||||
handler: async () => ({
|
||||
response: await helmService.listCharts(),
|
||||
}),
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(async () => ({
|
||||
response: await helmService.listCharts(),
|
||||
})),
|
||||
});
|
||||
|
||||
export default listChartsRouteInjectable;
|
||||
@@ -3,32 +3,23 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { clusterRoute } from "../../../router/route";
|
||||
|
||||
const deleteReleaseRouteInjectable = getInjectable({
|
||||
const deleteReleaseRouteInjectable = getRouteInjectable({
|
||||
id: "delete-release-route",
|
||||
|
||||
instantiate: (): Route<string> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "delete",
|
||||
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
|
||||
|
||||
handler: async (request) => {
|
||||
const { cluster, params } = request;
|
||||
|
||||
return {
|
||||
response: await helmService.deleteRelease(
|
||||
cluster,
|
||||
params.release,
|
||||
params.namespace,
|
||||
),
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(async ({ cluster, params: { release, namespace }}) => ({
|
||||
response: await helmService.deleteRelease(
|
||||
cluster,
|
||||
release,
|
||||
namespace,
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
export default deleteReleaseRouteInjectable;
|
||||
@@ -3,32 +3,23 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { clusterRoute } from "../../../router/route";
|
||||
|
||||
const getReleaseRouteHistoryInjectable = getInjectable({
|
||||
const getReleaseRouteHistoryInjectable = getRouteInjectable({
|
||||
id: "get-release-history-route",
|
||||
|
||||
instantiate: (): Route<any> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/v2/releases/{namespace}/{release}/history`,
|
||||
|
||||
handler: async (request) => {
|
||||
const { cluster, params } = request;
|
||||
|
||||
return {
|
||||
response: await helmService.getReleaseHistory(
|
||||
cluster,
|
||||
params.release,
|
||||
params.namespace,
|
||||
),
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(async ({ cluster, params }) => ({
|
||||
response: await helmService.getReleaseHistory(
|
||||
cluster,
|
||||
params.release,
|
||||
params.namespace,
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
export default getReleaseRouteHistoryInjectable;
|
||||
@@ -3,32 +3,23 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { clusterRoute } from "../../../router/route";
|
||||
|
||||
const getReleaseRouteInjectable = getInjectable({
|
||||
const getReleaseRouteInjectable = getRouteInjectable({
|
||||
id: "get-release-route",
|
||||
|
||||
instantiate: (): Route<any> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
|
||||
|
||||
handler: async (request) => {
|
||||
const { cluster, params } = request;
|
||||
|
||||
return {
|
||||
response: await helmService.getRelease(
|
||||
cluster,
|
||||
params.release,
|
||||
params.namespace,
|
||||
),
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(async ({ cluster, params }) => ({
|
||||
response: await helmService.getRelease(
|
||||
cluster,
|
||||
params.release,
|
||||
params.namespace,
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
export default getReleaseRouteInjectable;
|
||||
@@ -3,37 +3,27 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { getBoolean } from "../../../utils/parse-query";
|
||||
import { contentTypes } from "../../../router/router-content-types";
|
||||
import { clusterRoute } from "../../../router/route";
|
||||
|
||||
const getReleaseRouteValuesInjectable = getInjectable({
|
||||
const getReleaseRouteValuesInjectable = getRouteInjectable({
|
||||
id: "get-release-values-route",
|
||||
|
||||
instantiate: (): Route<string> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/v2/releases/{namespace}/{release}/values`,
|
||||
})(async ({ cluster, params: { namespace, release }, query }) => ({
|
||||
response: await helmService.getReleaseValues(release, {
|
||||
cluster,
|
||||
namespace,
|
||||
all: getBoolean(query, "all"),
|
||||
}),
|
||||
|
||||
handler: async (request) => {
|
||||
const { cluster, params: { namespace, release }, query } = request;
|
||||
const all = getBoolean(query, "all");
|
||||
|
||||
return {
|
||||
response: await helmService.getReleaseValues(release, {
|
||||
cluster,
|
||||
namespace,
|
||||
all,
|
||||
}),
|
||||
|
||||
contentType: contentTypes.txt,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
contentType: contentTypes.txt,
|
||||
})),
|
||||
});
|
||||
|
||||
export default getReleaseRouteValuesInjectable;
|
||||
@@ -3,34 +3,42 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import type { InstallChartArgs } from "../../../helm/helm-service";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import Joi from "joi";
|
||||
import { payloadValidatedClusterRoute } from "../../../router/route";
|
||||
|
||||
interface InstallChartResponse {
|
||||
log: string;
|
||||
release: { name: string; namespace: string };
|
||||
}
|
||||
const installChartArgsValidator = Joi.object<InstallChartArgs, true, InstallChartArgs>({
|
||||
chart: Joi
|
||||
.string()
|
||||
.required(),
|
||||
values: Joi
|
||||
.object()
|
||||
.required()
|
||||
.unknown(true),
|
||||
name: Joi
|
||||
.string()
|
||||
.required(),
|
||||
namespace: Joi
|
||||
.string()
|
||||
.required(),
|
||||
version: Joi
|
||||
.string()
|
||||
.required(),
|
||||
});
|
||||
|
||||
const installChartRouteInjectable = getInjectable({
|
||||
const installChartRouteInjectable = getRouteInjectable({
|
||||
id: "install-chart-route",
|
||||
|
||||
instantiate: () : Route<InstallChartResponse> => ({
|
||||
instantiate: () => payloadValidatedClusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/v2/releases`,
|
||||
|
||||
handler: async (request) => {
|
||||
const { payload, cluster } = request;
|
||||
|
||||
return {
|
||||
response: await helmService.installChart(cluster, payload),
|
||||
statusCode: 201,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
payloadValidator: installChartArgsValidator,
|
||||
})(async ({ payload, cluster }) => ({
|
||||
response: await helmService.installChart(cluster, payload),
|
||||
statusCode: 201,
|
||||
})),
|
||||
});
|
||||
|
||||
export default installChartRouteInjectable;
|
||||
@@ -3,28 +3,19 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { clusterRoute } from "../../../router/route";
|
||||
|
||||
const listReleasesRouteInjectable = getInjectable({
|
||||
const listReleasesRouteInjectable = getRouteInjectable({
|
||||
id: "list-releases-route",
|
||||
|
||||
instantiate: (): Route<Record<string, any>> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/v2/releases/{namespace?}`,
|
||||
|
||||
handler: async (request) => {
|
||||
const { cluster, params } = request;
|
||||
|
||||
return {
|
||||
response: await helmService.listReleases(cluster, params.namespace),
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(async ({ cluster, params }) => ({
|
||||
response: await helmService.listReleases(cluster, params.namespace),
|
||||
})),
|
||||
});
|
||||
|
||||
export default listReleasesRouteInjectable;
|
||||
@@ -3,26 +3,31 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import Joi from "joi";
|
||||
import { payloadValidatedClusterRoute } from "../../../router/route";
|
||||
|
||||
const rollbackReleaseRouteInjectable = getInjectable({
|
||||
interface RollbackReleasePayload {
|
||||
revision: number;
|
||||
}
|
||||
|
||||
const rollbackReleasePayloadValidator = Joi.object<RollbackReleasePayload, true, RollbackReleasePayload>({
|
||||
revision: Joi
|
||||
.number()
|
||||
.required(),
|
||||
});
|
||||
|
||||
const rollbackReleaseRouteInjectable = getRouteInjectable({
|
||||
id: "rollback-release-route",
|
||||
|
||||
instantiate: (): Route<void> => ({
|
||||
instantiate: () => payloadValidatedClusterRoute({
|
||||
method: "put",
|
||||
path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback`,
|
||||
|
||||
handler: async (request) => {
|
||||
const { cluster, params, payload } = request;
|
||||
|
||||
await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
|
||||
},
|
||||
payloadValidator: rollbackReleasePayloadValidator,
|
||||
})(async ({ cluster, params: { release, namespace }, payload }) => {
|
||||
await helmService.rollback(cluster, release, namespace, payload.revision);
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
});
|
||||
|
||||
export default rollbackReleaseRouteInjectable;
|
||||
@@ -3,38 +3,39 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { apiPrefix } from "../../../../common/vars";
|
||||
import type { Route } from "../../../router/router";
|
||||
import type { UpdateChartArgs } from "../../../helm/helm-service";
|
||||
import { helmService } from "../../../helm/helm-service";
|
||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRouteInjectable } from "../../../router/router.injectable";
|
||||
import { payloadValidatedClusterRoute } from "../../../router/route";
|
||||
import Joi from "joi";
|
||||
|
||||
interface UpdateReleaseResponse {
|
||||
log: string;
|
||||
release: { name: string; namespace: string };
|
||||
}
|
||||
const updateChartArgsValidator = Joi.object<UpdateChartArgs, true, UpdateChartArgs>({
|
||||
chart: Joi
|
||||
.string()
|
||||
.required(),
|
||||
version: Joi
|
||||
.string()
|
||||
.required(),
|
||||
values: Joi
|
||||
.object()
|
||||
.unknown(true),
|
||||
});
|
||||
|
||||
const updateReleaseRouteInjectable = getInjectable({
|
||||
const updateReleaseRouteInjectable = getRouteInjectable({
|
||||
id: "update-release-route",
|
||||
|
||||
instantiate: (): Route<UpdateReleaseResponse> => ({
|
||||
instantiate: () => payloadValidatedClusterRoute({
|
||||
method: "put",
|
||||
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
|
||||
|
||||
handler: async ({
|
||||
payloadValidator: updateChartArgsValidator,
|
||||
})(async ({ cluster, params, payload }) => ({
|
||||
response: await helmService.updateRelease(
|
||||
cluster,
|
||||
params,
|
||||
params.release,
|
||||
params.namespace,
|
||||
payload,
|
||||
}) => ({
|
||||
response: await helmService.updateRelease(
|
||||
cluster,
|
||||
params.release,
|
||||
params.namespace,
|
||||
payload,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
export default updateReleaseRouteInjectable;
|
||||
@@ -3,43 +3,88 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import type { Route } from "../../router/router";
|
||||
import { routeInjectionToken } from "../../router/router.injectable";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
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";
|
||||
|
||||
const getServiceAccountRouteInjectable = getInjectable({
|
||||
const getServiceAccountRouteInjectable = getRouteInjectable({
|
||||
id: "get-service-account-route",
|
||||
|
||||
instantiate: (): Route<ReturnType<typeof generateKubeConfig>> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
|
||||
})(async ({ params, cluster }) => {
|
||||
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
const secretList = await client.listNamespacedSecret(params.namespace);
|
||||
|
||||
handler: async (request) => {
|
||||
const { params, cluster } = request;
|
||||
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
const secretList = await client.listNamespacedSecret(params.namespace);
|
||||
const secret = secretList.body.items.find(secret => {
|
||||
const { annotations } = secret.metadata ?? {};
|
||||
|
||||
const secret = secretList.body.items.find(secret => {
|
||||
const { annotations } = secret.metadata;
|
||||
return annotations?.["kubernetes.io/service-account.name"] === params.account;
|
||||
});
|
||||
|
||||
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
|
||||
});
|
||||
if (!secret) {
|
||||
return {
|
||||
error: "No secret found",
|
||||
statusCode: 404,
|
||||
};
|
||||
}
|
||||
|
||||
return { response: generateKubeConfig(params.account, secret, cluster) };
|
||||
},
|
||||
const kubeconfig = generateKubeConfig(params.account, secret, cluster);
|
||||
|
||||
if (!kubeconfig) {
|
||||
return {
|
||||
error: "No secret found",
|
||||
statusCode: 404,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: kubeconfig,
|
||||
};
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
});
|
||||
|
||||
export default getServiceAccountRouteInjectable;
|
||||
|
||||
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
|
||||
const tokenData = Buffer.from(secret.data["token"], "base64");
|
||||
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 {
|
||||
if (!secret.data || !secret.metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { token, "ca.crt": caCrt } = secret.data;
|
||||
|
||||
const tokenData = Buffer.from(token, "base64");
|
||||
|
||||
return {
|
||||
"apiVersion": "v1",
|
||||
@@ -49,7 +94,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
|
||||
"name": cluster.contextName,
|
||||
"cluster": {
|
||||
"server": cluster.apiUrl,
|
||||
"certificate-authority-data": secret.data["ca.crt"],
|
||||
"certificate-authority-data": caCrt,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import type { LensApiRequest, Route } from "../../router/router";
|
||||
import { routeInjectionToken } from "../../router/router.injectable";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster-types";
|
||||
import logger from "../../logger";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { getMetrics } from "../../k8s-request";
|
||||
import type { IMetricsQuery } from "./metrics-query";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
import { isObject } from "lodash";
|
||||
import { isRequestError } from "../../../common/utils";
|
||||
|
||||
// This is used for backoff retry tracking.
|
||||
const ATTEMPTS = [false, false, false, false, true];
|
||||
@@ -27,8 +28,13 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
|
||||
try {
|
||||
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
|
||||
} catch (error) {
|
||||
if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) {
|
||||
logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error);
|
||||
if (isRequestError(error)) {
|
||||
if (lastAttempt || (error.statusCode && error.statusCode >= 400 && error.statusCode < 500)) {
|
||||
logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error);
|
||||
throw new Error("Metrics not available");
|
||||
}
|
||||
} else {
|
||||
logger.error("[Metrics]: metrics not available", error);
|
||||
throw new Error("Metrics not available");
|
||||
}
|
||||
|
||||
@@ -43,67 +49,66 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
|
||||
return Promise.all(queries.map(loadMetric));
|
||||
}
|
||||
|
||||
const addMetricsRoute = async ({ cluster, payload, query }: LensApiRequest) => {
|
||||
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
|
||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
||||
|
||||
try {
|
||||
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails();
|
||||
|
||||
prometheusMetadata.provider = provider?.id;
|
||||
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
||||
|
||||
if (!prometheusPath) {
|
||||
prometheusMetadata.success = false;
|
||||
|
||||
return { response: {}};
|
||||
}
|
||||
|
||||
// return data in same structure as query
|
||||
if (typeof payload === "string") {
|
||||
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
|
||||
|
||||
return { response: data };
|
||||
}
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
|
||||
|
||||
return { response: data };
|
||||
}
|
||||
|
||||
const queries = Object.entries<Record<string, string>>(payload)
|
||||
.map(([queryName, queryOpts]) => (
|
||||
provider.getQuery(queryOpts, queryName)
|
||||
));
|
||||
|
||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
|
||||
|
||||
prometheusMetadata.success = true;
|
||||
|
||||
return { response: data };
|
||||
} catch (error) {
|
||||
prometheusMetadata.success = false;
|
||||
|
||||
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error);
|
||||
|
||||
return { response: {}};
|
||||
} finally {
|
||||
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
||||
}
|
||||
};
|
||||
|
||||
const addMetricsRouteInjectable = getInjectable({
|
||||
const addMetricsRouteInjectable = getRouteInjectable({
|
||||
id: "add-metrics-route",
|
||||
|
||||
instantiate: (): Route<any> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/metrics`,
|
||||
handler: addMetricsRoute,
|
||||
}),
|
||||
})(async ({ cluster, payload, query }) => {
|
||||
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
|
||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
try {
|
||||
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails();
|
||||
|
||||
prometheusMetadata.provider = provider?.id;
|
||||
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
||||
|
||||
if (!prometheusPath) {
|
||||
prometheusMetadata.success = false;
|
||||
|
||||
return { response: {}};
|
||||
}
|
||||
|
||||
// return data in same structure as query
|
||||
if (typeof payload === "string") {
|
||||
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
|
||||
|
||||
return { response: data };
|
||||
}
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
|
||||
|
||||
return { response: data };
|
||||
}
|
||||
|
||||
if (isObject(payload)) {
|
||||
const queries = Object.entries(payload as Record<string, Record<string, string>>)
|
||||
.map(([queryName, queryOpts]) => (
|
||||
provider.getQuery(queryOpts, queryName)
|
||||
));
|
||||
|
||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
|
||||
|
||||
prometheusMetadata.success = true;
|
||||
|
||||
return { response: data };
|
||||
}
|
||||
|
||||
return { response: {}};
|
||||
} catch (error) {
|
||||
prometheusMetadata.success = false;
|
||||
|
||||
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error);
|
||||
|
||||
return { response: {}};
|
||||
} finally {
|
||||
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
export default addMetricsRouteInjectable;
|
||||
@@ -3,32 +3,26 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import type { Route } from "../../router/router";
|
||||
import { routeInjectionToken } from "../../router/router.injectable";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { PrometheusProviderRegistry } from "../../prometheus";
|
||||
import type { MetricProviderInfo } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { route } from "../../router/route";
|
||||
|
||||
const getMetricProvidersRouteInjectable = getInjectable({
|
||||
const getMetricProvidersRouteInjectable = getRouteInjectable({
|
||||
id: "get-metric-providers-route",
|
||||
|
||||
instantiate: (): Route<MetricProviderInfo[]> => ({
|
||||
instantiate: () => route({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/metrics/providers`,
|
||||
|
||||
handler: () => {
|
||||
const providers: MetricProviderInfo[] = [];
|
||||
|
||||
for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) {
|
||||
providers.push({ name, id, isConfigurable });
|
||||
}
|
||||
|
||||
return { response: providers };
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(() => ({
|
||||
response: Array.from(
|
||||
PrometheusProviderRegistry
|
||||
.getInstance()
|
||||
.providers
|
||||
.values(),
|
||||
({ name, id, isConfigurable }) => ({ name, id, isConfigurable }),
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
export default getMetricProvidersRouteInjectable;
|
||||
@@ -7,8 +7,9 @@ import { getPortFrom } from "../../../utils/get-port";
|
||||
import type { ChildProcessWithoutNullStreams } from "child_process";
|
||||
import { spawn } from "child_process";
|
||||
import * as tcpPortUsed from "tcp-port-used";
|
||||
import { TypedRegEx } from "typed-regex";
|
||||
|
||||
const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
|
||||
const internalPortRegex = TypedRegEx("^forwarding from (?<address>.+) ->", "i");
|
||||
|
||||
export interface PortForwardArgs {
|
||||
clusterId: string;
|
||||
@@ -36,7 +37,7 @@ export class PortForward {
|
||||
));
|
||||
}
|
||||
|
||||
public process: ChildProcessWithoutNullStreams;
|
||||
public process?: ChildProcessWithoutNullStreams;
|
||||
public clusterId: string;
|
||||
public kind: string;
|
||||
public namespace: string;
|
||||
@@ -98,6 +99,6 @@ export class PortForward {
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
this.process.kill();
|
||||
this.process?.kill();
|
||||
}
|
||||
}
|
||||
@@ -2,36 +2,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 { routeInjectionToken } from "../../router/router.injectable";
|
||||
import type { LensApiRequest, Route } from "../../router/router";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import { PortForward } from "./functionality/port-forward";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
|
||||
const getCurrentPortForward = async (request: LensApiRequest) => {
|
||||
const { params, query, cluster } = request;
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
|
||||
const portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||
namespace, port, forwardPort,
|
||||
});
|
||||
|
||||
return { response: { port: portForward?.forwardPort ?? null }};
|
||||
};
|
||||
|
||||
const getCurrentPortForwardRouteInjectable = getInjectable({
|
||||
const getCurrentPortForwardRouteInjectable = getRouteInjectable({
|
||||
id: "get-current-port-forward-route",
|
||||
|
||||
instantiate: (): Route<{ port: number }> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
|
||||
handler: getCurrentPortForward,
|
||||
}),
|
||||
})(async ({ params, query, cluster }) => {
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
const portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||
namespace, port, forwardPort,
|
||||
});
|
||||
|
||||
return {
|
||||
response: {
|
||||
port: portForward?.forwardPort ?? null,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
export default getCurrentPortForwardRouteInjectable;
|
||||
@@ -2,97 +2,90 @@
|
||||
* 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 { routeInjectionToken } from "../../router/router.injectable";
|
||||
import type { LensApiRequest, Route } from "../../router/router";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import type { PortForwardArgs } from "./functionality/port-forward";
|
||||
import { PortForward } from "./functionality/port-forward";
|
||||
import logger from "../../logger";
|
||||
import createPortForwardInjectable from "./functionality/create-port-forward.injectable";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
|
||||
interface Dependencies {
|
||||
createPortForward: (pathToKubeConfig: string, args: PortForwardArgs) => PortForward;
|
||||
}
|
||||
const startPortForwardRouteInjectable = getRouteInjectable({
|
||||
id: "start-current-port-forward-route",
|
||||
|
||||
const startPortForward = ({ createPortForward }: Dependencies) => async (request: LensApiRequest) => {
|
||||
const { params, query, cluster } = request;
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
instantiate: (di) => {
|
||||
const createPortForward = di.inject(createPortForwardInjectable);
|
||||
|
||||
try {
|
||||
let portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id,
|
||||
kind: resourceType,
|
||||
name: resourceName,
|
||||
namespace,
|
||||
port,
|
||||
forwardPort,
|
||||
});
|
||||
return clusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
|
||||
})(async ({ params, query, cluster }) => {
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const started = await portForward.start();
|
||||
|
||||
if (!started) {
|
||||
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", {
|
||||
try {
|
||||
let portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id,
|
||||
kind: resourceType,
|
||||
name: resourceName,
|
||||
namespace,
|
||||
port,
|
||||
resourceType,
|
||||
resourceName,
|
||||
forwardPort,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const started = await portForward.start();
|
||||
|
||||
if (!started) {
|
||||
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", {
|
||||
namespace,
|
||||
port,
|
||||
resourceType,
|
||||
resourceName,
|
||||
});
|
||||
|
||||
return {
|
||||
error: {
|
||||
message: `Failed to forward port ${port} to ${
|
||||
thePort ? forwardPort : "random port"
|
||||
}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { response: { port: portForward.forwardPort }};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`,
|
||||
{ namespace, port, resourceType, resourceName },
|
||||
);
|
||||
|
||||
return {
|
||||
error: {
|
||||
message: `Failed to forward port ${port} to ${
|
||||
thePort ? forwardPort : "random port"
|
||||
}`,
|
||||
message: `Failed to forward port ${port}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { response: { port: portForward.forwardPort }};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`,
|
||||
{ namespace, port, resourceType, resourceName },
|
||||
);
|
||||
|
||||
return {
|
||||
error: {
|
||||
message: `Failed to forward port ${port}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const startPortForwardRouteInjectable = getInjectable({
|
||||
id: "start-current-port-forward-route",
|
||||
|
||||
instantiate: (di): Route<{ port: number }> => ({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
|
||||
handler: startPortForward({ createPortForward: di.inject(createPortForwardInjectable) }),
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default startPortForwardRouteInjectable;
|
||||
@@ -2,50 +2,42 @@
|
||||
* 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 { routeInjectionToken } from "../../router/router.injectable";
|
||||
import type { LensApiRequest, Route } from "../../router/router";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import { PortForward } from "./functionality/port-forward";
|
||||
import logger from "../../logger";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
|
||||
const stopCurrentPortForward = async (request: LensApiRequest) => {
|
||||
const { params, query, cluster } = request;
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
|
||||
const portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||
namespace, port, forwardPort,
|
||||
});
|
||||
|
||||
try {
|
||||
await portForward.stop();
|
||||
|
||||
return { response: { status: true }};
|
||||
} catch (error) {
|
||||
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
|
||||
|
||||
return {
|
||||
error: {
|
||||
message: `error stopping a forward port ${port}`,
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const stopCurrentPortForwardRouteInjectable = getInjectable({
|
||||
const stopCurrentPortForwardRouteInjectable = getRouteInjectable({
|
||||
id: "stop-current-port-forward-route",
|
||||
|
||||
instantiate: (): Route<{ status: boolean }> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "delete",
|
||||
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
|
||||
handler: stopCurrentPortForward,
|
||||
}),
|
||||
})(async ({ params, query, cluster }) => {
|
||||
const { namespace, resourceType, resourceName } = params;
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
const portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||
namespace, port, forwardPort,
|
||||
});
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
try {
|
||||
await portForward?.stop();
|
||||
|
||||
return { response: { status: true }};
|
||||
} catch (error) {
|
||||
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
|
||||
|
||||
return {
|
||||
error: {
|
||||
message: `error stopping a forward port ${port}`,
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
export default stopCurrentPortForwardRouteInjectable;
|
||||
@@ -2,25 +2,20 @@
|
||||
* 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 { routeInjectionToken } from "../../router/router.injectable";
|
||||
import type { Route } from "../../router/router";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import { ResourceApplier } from "../../resource-applier";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
|
||||
const applyResourceRouteInjectable = getInjectable({
|
||||
const applyResourceRouteInjectable = getRouteInjectable({
|
||||
id: "apply-resource-route",
|
||||
|
||||
instantiate: (): Route<string> => ({
|
||||
instantiate: () => clusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/stack`,
|
||||
|
||||
handler: async ({ cluster, payload }) => ({
|
||||
response: await new ResourceApplier(cluster).apply(payload),
|
||||
}),
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await new ResourceApplier(cluster).apply(payload),
|
||||
})),
|
||||
});
|
||||
|
||||
export default applyResourceRouteInjectable;
|
||||
@@ -2,30 +2,56 @@
|
||||
* 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 { routeInjectionToken } from "../../router/router.injectable";
|
||||
import type { Route } from "../../router/router";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import { ResourceApplier } from "../../resource-applier";
|
||||
import { payloadValidatedClusterRoute } from "../../router/route";
|
||||
import Joi from "joi";
|
||||
import type { Patch } from "rfc6902";
|
||||
|
||||
const patchResourceRouteInjectable = getInjectable({
|
||||
interface PatchResourcePayload {
|
||||
name: string;
|
||||
kind: string;
|
||||
patch: Patch;
|
||||
ns?: string;
|
||||
}
|
||||
|
||||
const patchResourcePayloadValidator = Joi.object<PatchResourcePayload, true, PatchResourcePayload>({
|
||||
name: Joi
|
||||
.string()
|
||||
.required(),
|
||||
kind: Joi
|
||||
.string()
|
||||
.required(),
|
||||
ns: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
patch: Joi
|
||||
.array()
|
||||
.allow(
|
||||
Joi.object({
|
||||
op: Joi
|
||||
.string()
|
||||
.required(),
|
||||
}).unknown(true),
|
||||
),
|
||||
});
|
||||
|
||||
const patchResourceRouteInjectable = getRouteInjectable({
|
||||
id: "patch-resource-route",
|
||||
|
||||
instantiate: (): Route<string> => ({
|
||||
instantiate: () => payloadValidatedClusterRoute({
|
||||
method: "patch",
|
||||
path: `${apiPrefix}/stack`,
|
||||
|
||||
handler: async ({ cluster, payload }) => ({
|
||||
response: await new ResourceApplier(cluster).patch(
|
||||
payload.name,
|
||||
payload.kind,
|
||||
payload.patch,
|
||||
payload.ns,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
payloadValidator: patchResourcePayloadValidator,
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await new ResourceApplier(cluster).patch(
|
||||
payload.name,
|
||||
payload.kind,
|
||||
payload.patch,
|
||||
payload.ns,
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
export default patchResourceRouteInjectable;
|
||||
@@ -2,12 +2,10 @@
|
||||
* 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 { LensApiRequest, Route } from "../router/router";
|
||||
import type { SupportedFileExtension } from "../router/router-content-types";
|
||||
import { contentTypes } from "../router/router-content-types";
|
||||
import logger from "../logger";
|
||||
import { routeInjectionToken } from "../router/router.injectable";
|
||||
import { getRouteInjectable } from "../router/router.injectable";
|
||||
import { appName, publicPath, staticFilesDirectory } from "../../common/vars";
|
||||
import path from "path";
|
||||
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
||||
@@ -18,6 +16,8 @@ import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injec
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import { webpackDevServerPort } from "../../../webpack/vars";
|
||||
import type { LensApiRequest, RouteResponse } from "../router/route";
|
||||
import { route } from "../router/route";
|
||||
|
||||
interface ProductionDependencies {
|
||||
readFileBuffer: (path: string) => Promise<Buffer>;
|
||||
@@ -27,7 +27,7 @@ interface ProductionDependencies {
|
||||
|
||||
const handleStaticFileInProduction =
|
||||
({ readFileBuffer, getAbsolutePath, joinPaths }: ProductionDependencies) =>
|
||||
async ({ params }: LensApiRequest) => {
|
||||
async ({ params }: LensApiRequest<"/{path*}">): Promise<RouteResponse<Buffer>> => {
|
||||
const staticPath = getAbsolutePath(staticFilesDirectory);
|
||||
let filePath = params.path;
|
||||
|
||||
@@ -49,7 +49,7 @@ const handleStaticFileInProduction =
|
||||
return { response: await readFileBuffer(asset), contentType };
|
||||
} catch (err) {
|
||||
if (retryCount > 5) {
|
||||
logger.error("handleStaticFile:", err.toString());
|
||||
logger.error("handleStaticFile:", String(err));
|
||||
|
||||
return { statusCode: 404 };
|
||||
}
|
||||
@@ -67,10 +67,8 @@ interface DevelopmentDependencies {
|
||||
|
||||
const handleStaticFileInDevelopment =
|
||||
({ proxy }: DevelopmentDependencies) =>
|
||||
(apiReq: LensApiRequest) => {
|
||||
const { req, res } = apiReq.raw;
|
||||
|
||||
if (req.url === "/" || !req.url.startsWith("/build/")) {
|
||||
({ raw: { req, res }}: LensApiRequest<"/{path*}">): RouteResponse<Buffer> => {
|
||||
if (req.url === "/" || !req.url?.startsWith("/build/")) {
|
||||
req.url = `${publicPath}/${appName}.html`;
|
||||
}
|
||||
|
||||
@@ -81,25 +79,27 @@ const handleStaticFileInDevelopment =
|
||||
return { proxy };
|
||||
};
|
||||
|
||||
const staticFileRouteInjectable = getInjectable({
|
||||
const staticFileRouteInjectable = getRouteInjectable({
|
||||
id: "static-file-route",
|
||||
|
||||
instantiate: (di): Route<Buffer> => {
|
||||
instantiate: (di) => {
|
||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||
const readFileBuffer = di.inject(readFileBufferInjectable);
|
||||
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
|
||||
return {
|
||||
return route({
|
||||
method: "get",
|
||||
path: `/{path*}`,
|
||||
handler: isDevelopment
|
||||
? handleStaticFileInDevelopment({ proxy: httpProxy.createProxy() })
|
||||
: handleStaticFileInProduction({ readFileBuffer, getAbsolutePath, joinPaths }),
|
||||
};
|
||||
})(
|
||||
isDevelopment
|
||||
? handleStaticFileInDevelopment({
|
||||
proxy: httpProxy.createProxy(),
|
||||
})
|
||||
: handleStaticFileInProduction({
|
||||
readFileBuffer: di.inject(readFileBufferInjectable),
|
||||
getAbsolutePath: di.inject(getAbsolutePathInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
});
|
||||
|
||||
export default staticFileRouteInjectable;
|
||||
@@ -2,22 +2,21 @@
|
||||
* 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 { Route } from "../../router/router";
|
||||
import { routeInjectionToken } from "../../router/router.injectable";
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { getAppVersion } from "../../../common/utils";
|
||||
import { route } from "../../router/route";
|
||||
|
||||
const getVersionRouteInjectable = getInjectable({
|
||||
const getVersionRouteInjectable = getRouteInjectable({
|
||||
id: "get-version-route",
|
||||
|
||||
instantiate: (): Route<{ version: string }> => ({
|
||||
instantiate: () => route({
|
||||
method: "get",
|
||||
path: `/version`,
|
||||
|
||||
handler: () => ({ response: { version: getAppVersion() }}),
|
||||
}),
|
||||
|
||||
injectionToken: routeInjectionToken,
|
||||
})(() => ({
|
||||
response: {
|
||||
version: getAppVersion(),
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
export default getVersionRouteInjectable;
|
||||
@@ -15,7 +15,7 @@ import { baseBinariesDir } from "../../../common/vars";
|
||||
export class LocalShellSession extends ShellSession {
|
||||
ShellType = "shell";
|
||||
|
||||
constructor(protected shellEnvModify: (clusterId: ClusterId, env: Record<string, string>) => Record<string, string>, kubectl: Kubectl, websocket: WebSocket, cluster: Cluster, terminalId: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -34,9 +34,14 @@ export class LocalShellSession extends ShellSession {
|
||||
env = this.shellEnvModify(this.cluster.id, env);
|
||||
|
||||
const shell = env.PTYSHELL;
|
||||
|
||||
if (!shell) {
|
||||
throw new Error("PTYSHELL is not defined with the environment");
|
||||
}
|
||||
|
||||
const args = await this.getShellArgs(shell);
|
||||
|
||||
await this.openShellProcess(env.PTYSHELL, args, env);
|
||||
await this.openShellProcess(shell, args, env);
|
||||
}
|
||||
|
||||
protected async getShellArgs(shell: string): Promise<string[]> {
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
import type WebSocket from "ws";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import { Watch, CoreV1Api } from "@kubernetes/client-node";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
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 { 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 { TerminalChannels } from "../../../renderer/api/terminal-api";
|
||||
@@ -20,7 +20,6 @@ export class NodeShellSession extends ShellSession {
|
||||
ShellType = "node-shell";
|
||||
|
||||
protected readonly podName = `node-shell-${uuid()}`;
|
||||
protected kc: KubeConfig;
|
||||
|
||||
protected readonly cwd: string | undefined = undefined;
|
||||
|
||||
@@ -29,29 +28,49 @@ export class NodeShellSession extends ShellSession {
|
||||
}
|
||||
|
||||
public async open() {
|
||||
this.kc = await this.cluster.getProxyKubeconfig();
|
||||
const kc = await this.cluster.getProxyKubeconfig();
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const shell = await this.kubectl.getPath();
|
||||
|
||||
const cleanup = once(() => {
|
||||
coreApi
|
||||
.deleteNamespacedPod(this.podName, "kube-system")
|
||||
.catch(error => logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
|
||||
});
|
||||
|
||||
this.websocket.once("close", cleanup);
|
||||
|
||||
try {
|
||||
await this.createNodeShellPod();
|
||||
await this.waitForRunningPod();
|
||||
await this.createNodeShellPod(coreApi);
|
||||
await this.waitForRunningPod(kc);
|
||||
} catch (error) {
|
||||
this.deleteNodeShellPod();
|
||||
cleanup();
|
||||
|
||||
this.send({
|
||||
type: TerminalChannels.STDOUT,
|
||||
data: `Error occurred: ${get(error, "response.body.message", error?.toString() || "unknown error")}`,
|
||||
data: `Error occurred: ${get(error, "response.body.message", error ? String(error) : "unknown error")}`,
|
||||
});
|
||||
|
||||
throw new ShellOpenError("failed to create node pod", error);
|
||||
throw new ShellOpenError(
|
||||
"failed to create node pod",
|
||||
error instanceof Error
|
||||
? { cause: error }
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const env = await this.getCachedShellEnv();
|
||||
const args = ["exec", "-i", "-t", "-n", "kube-system", this.podName, "--"];
|
||||
const nodeApi = new NodesApi({
|
||||
const nodeApi = new NodeApi({
|
||||
objectConstructor: Node,
|
||||
request: KubeJsonApi.forCluster(this.cluster.id),
|
||||
});
|
||||
const node = await nodeApi.get({ name: this.nodeName });
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`No node with name=${this.nodeName} found`);
|
||||
}
|
||||
|
||||
const nodeOs = node.getOperatingSystem();
|
||||
|
||||
switch (nodeOs) {
|
||||
@@ -69,16 +88,14 @@ export class NodeShellSession extends ShellSession {
|
||||
await this.openShellProcess(shell, args, env);
|
||||
}
|
||||
|
||||
protected createNodeShellPod() {
|
||||
protected createNodeShellPod(coreApi: CoreV1Api) {
|
||||
const imagePullSecrets = this.cluster.imagePullSecret
|
||||
? [{
|
||||
name: this.cluster.imagePullSecret,
|
||||
}]
|
||||
: undefined;
|
||||
|
||||
return this
|
||||
.kc
|
||||
.makeApiClient(k8s.CoreV1Api)
|
||||
return coreApi
|
||||
.createNamespacedPod("kube-system", {
|
||||
metadata: {
|
||||
name: this.podName,
|
||||
@@ -109,11 +126,11 @@ export class NodeShellSession extends ShellSession {
|
||||
});
|
||||
}
|
||||
|
||||
protected waitForRunningPod(): Promise<void> {
|
||||
protected waitForRunningPod(kc: KubeConfig): Promise<void> {
|
||||
logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
new k8s.Watch(this.kc)
|
||||
new Watch(kc)
|
||||
.watch(`/api/v1/namespaces/kube-system/pods`,
|
||||
{},
|
||||
// callback is called for each received object.
|
||||
@@ -146,17 +163,4 @@ export class NodeShellSession extends ShellSession {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected exit() {
|
||||
super.exit();
|
||||
this.deleteNodeShellPod();
|
||||
}
|
||||
|
||||
protected deleteNodeShellPod() {
|
||||
this
|
||||
.kc
|
||||
.makeApiClient(k8s.CoreV1Api)
|
||||
.deleteNamespacedPod(this.podName, "kube-system")
|
||||
.catch(error => logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,20 @@
|
||||
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 { catalogEntityRegistry } from "../../catalog";
|
||||
|
||||
|
||||
interface Dependencies {
|
||||
extensions: IComputedValue<LensMainExtension[]>;
|
||||
}
|
||||
|
||||
export const terminalShellEnvModify = ({ extensions }: Dependencies) =>
|
||||
(clusterId: ClusterId, env: Record<string, string>) => {
|
||||
|
||||
export const terminalShellEnvModify = ({ extensions }: Dependencies) =>
|
||||
(clusterId: ClusterId, env: Record<string, string | undefined>) => {
|
||||
const terminalShellEnvModifiers = computed(() => (
|
||||
extensions.get()
|
||||
.map((extension) => extension.terminalShellEnvModifier)
|
||||
.filter(Boolean)
|
||||
.filter(isDefined)
|
||||
))
|
||||
.get();
|
||||
|
||||
@@ -26,7 +27,7 @@ export const terminalShellEnvModify = ({ extensions }: Dependencies) =>
|
||||
return env;
|
||||
}
|
||||
|
||||
const entity = catalogEntityRegistry.getById(clusterId);
|
||||
const entity = catalogEntityRegistry.findById(clusterId);
|
||||
|
||||
if (entity) {
|
||||
const ctx = { catalogEntity: entity };
|
||||
|
||||
@@ -20,10 +20,11 @@ import type { TerminalMessage } from "../../renderer/api/terminal-api";
|
||||
import { TerminalChannels } from "../../renderer/api/terminal-api";
|
||||
import { deserialize, serialize } from "v8";
|
||||
import { stat } from "fs/promises";
|
||||
import { getOrInsertWith } from "../../common/utils";
|
||||
|
||||
export class ShellOpenError extends Error {
|
||||
constructor(message: string, public cause: Error) {
|
||||
super(`${message}: ${cause}`);
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(`${message}`, options);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this);
|
||||
}
|
||||
@@ -106,10 +107,10 @@ export enum WebSocketCloseEvent {
|
||||
}
|
||||
|
||||
export abstract class ShellSession {
|
||||
abstract ShellType: string;
|
||||
abstract readonly ShellType: string;
|
||||
|
||||
private static shellEnvs = new Map<string, Record<string, string>>();
|
||||
private static processes = new Map<string, pty.IPty>();
|
||||
private static readonly shellEnvs = new Map<string, Record<string, string | undefined>>();
|
||||
private static readonly processes = new Map<string, pty.IPty>();
|
||||
|
||||
/**
|
||||
* Kill all remaining shell backing processes. Should be called when about to
|
||||
@@ -128,35 +129,32 @@ export abstract class ShellSession {
|
||||
}
|
||||
|
||||
protected running = false;
|
||||
protected kubectlBinDirP: Promise<string>;
|
||||
protected kubeconfigPathP: Promise<string>;
|
||||
protected readonly kubectlBinDirP: Promise<string>;
|
||||
protected readonly kubeconfigPathP: Promise<string>;
|
||||
protected readonly terminalId: string;
|
||||
|
||||
protected abstract get cwd(): string | undefined;
|
||||
|
||||
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
|
||||
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string | undefined>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
|
||||
const resume = ShellSession.processes.has(this.terminalId);
|
||||
|
||||
if (!resume) {
|
||||
ShellSession.processes.set(this.terminalId, pty.spawn(shell, args, {
|
||||
const shellProcess = getOrInsertWith(ShellSession.processes, this.terminalId, () => (
|
||||
pty.spawn(shell, args, {
|
||||
rows: 30,
|
||||
cols: 80,
|
||||
cwd,
|
||||
env,
|
||||
env: env as Record<string, string>,
|
||||
name: "xterm-256color",
|
||||
// TODO: Something else is broken here so we need to force the use of winPty on windows
|
||||
useConpty: false,
|
||||
}));
|
||||
}
|
||||
|
||||
const shellProcess = ShellSession.processes.get(this.terminalId);
|
||||
})
|
||||
));
|
||||
|
||||
logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`);
|
||||
|
||||
return { shellProcess, resume };
|
||||
}
|
||||
|
||||
constructor(protected kubectl: Kubectl, protected websocket: WebSocket, protected cluster: Cluster, terminalId: string) {
|
||||
constructor(protected readonly kubectl: Kubectl, protected readonly websocket: WebSocket, protected readonly cluster: Cluster, terminalId: string) {
|
||||
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
||||
this.kubectlBinDirP = this.kubectl.binDir();
|
||||
this.terminalId = `${cluster.id}:${terminalId}`;
|
||||
@@ -166,7 +164,7 @@ export abstract class ShellSession {
|
||||
this.websocket.send(serialize(message));
|
||||
}
|
||||
|
||||
protected async getCwd(env: Record<string, string>): Promise<string> {
|
||||
protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
|
||||
const cwdOptions = [this.cwd];
|
||||
|
||||
if (isWindows) {
|
||||
@@ -207,7 +205,7 @@ export abstract class ShellSession {
|
||||
return "."; // Always valid
|
||||
}
|
||||
|
||||
protected async openShellProcess(shell: string, args: string[], env: Record<string, string>) {
|
||||
protected async openShellProcess(shell: string, args: string[], env: Record<string, string | undefined>) {
|
||||
const cwd = await this.getCwd(env);
|
||||
const { shellProcess, resume } = this.ensureShellProcess(shell, args, env, cwd);
|
||||
|
||||
@@ -234,7 +232,7 @@ export abstract class ShellSession {
|
||||
});
|
||||
|
||||
this.websocket
|
||||
.on("message", (data: string | Uint8Array) => {
|
||||
.on("message", (data: string | Uint8Array): void => {
|
||||
if (!this.running) {
|
||||
return void logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
|
||||
}
|
||||
@@ -261,7 +259,7 @@ export abstract class ShellSession {
|
||||
logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error);
|
||||
}
|
||||
})
|
||||
.on("close", code => {
|
||||
.once("close", code => {
|
||||
logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
|
||||
|
||||
const stopShellSession = this.running
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 { storesAndApisCanBeCreatedInjectionToken } from "../common/k8s-api/stores-apis-can-be-created.token";
|
||||
|
||||
const storesAndApisCanBeCreatedInjectable = getInjectable({
|
||||
id: "create-stores-and-apis",
|
||||
instantiate: () => false,
|
||||
injectionToken: storesAndApisCanBeCreatedInjectionToken,
|
||||
});
|
||||
|
||||
export default storesAndApisCanBeCreatedInjectable;
|
||||
@@ -22,7 +22,7 @@ import path from "path";
|
||||
const TRAY_LOG_PREFIX = "[TRAY]";
|
||||
|
||||
// note: instance of Tray should be saved somewhere, otherwise it disappears
|
||||
export let tray: Tray;
|
||||
export let tray: Tray | null = null;
|
||||
|
||||
function getTrayIconPath(): string {
|
||||
return path.resolve(
|
||||
@@ -56,7 +56,7 @@ export function initTray(
|
||||
try {
|
||||
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences);
|
||||
|
||||
tray.setContextMenu(menu);
|
||||
tray?.setContextMenu(menu);
|
||||
} catch (error) {
|
||||
logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error });
|
||||
}
|
||||
@@ -73,7 +73,7 @@ function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron
|
||||
...trayItem,
|
||||
submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined,
|
||||
click: trayItem.click ? () => {
|
||||
trayItem.click(trayItem);
|
||||
trayItem.click?.(trayItem);
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,15 @@ interface GetPortArgs {
|
||||
* Should be case insensitive
|
||||
* Must have a named matching group called `address`
|
||||
*/
|
||||
lineRegex: RegExp;
|
||||
lineRegex: {
|
||||
match: (line: string) => {
|
||||
matched: boolean;
|
||||
groups?: {
|
||||
address?: string;
|
||||
};
|
||||
raw?: RegExpExecArray;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Called when the port is found
|
||||
*/
|
||||
@@ -35,15 +43,15 @@ export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number
|
||||
const logLines: string[] = [];
|
||||
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const handler = (data: any) => {
|
||||
const logItem: string = data.toString();
|
||||
const match = logItem.match(args.lineRegex);
|
||||
const handler = (data: unknown) => {
|
||||
const logItem = String(data);
|
||||
const match = args.lineRegex.match(logItem);
|
||||
|
||||
logLines.push(logItem);
|
||||
|
||||
if (match) {
|
||||
if (match.matched) {
|
||||
// use unknown protocol so that there is no default port
|
||||
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
||||
const addr = new URLParse(`s://${match.groups?.address?.trim()}`);
|
||||
|
||||
args.onFind?.();
|
||||
stream.off("data", handler);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* @param channel The current channel that did not have a new version associated with it
|
||||
* @returns The channel name of the next release version
|
||||
*/
|
||||
export function nextUpdateChannel(defaultChannel: string, channel: string): string {
|
||||
export function nextUpdateChannel(defaultChannel: string, channel: string | null): string {
|
||||
switch (channel) {
|
||||
case "alpha":
|
||||
return "beta";
|
||||
|
||||
+54
-52
@@ -18,10 +18,6 @@ import { isMac, productName } from "../common/vars";
|
||||
import { LensProxy } from "./lens-proxy";
|
||||
import { bundledExtensionsLoaded } from "../common/ipc/extension-handling";
|
||||
|
||||
function isHideable(window: BrowserWindow | null): boolean {
|
||||
return Boolean(window && !window.isDestroyed());
|
||||
}
|
||||
|
||||
export interface SendToViewArgs {
|
||||
channel: string;
|
||||
frameInfo?: ClusterFrameInfo;
|
||||
@@ -31,12 +27,11 @@ export interface SendToViewArgs {
|
||||
export class WindowManager extends Singleton {
|
||||
public mainContentUrl = `http://localhost:${LensProxy.getInstance().port}`;
|
||||
|
||||
protected mainWindow: BrowserWindow;
|
||||
protected splashWindow: BrowserWindow;
|
||||
protected windowState: windowStateKeeper.State;
|
||||
protected disposers: Record<string, Function> = {};
|
||||
protected mainWindow?: BrowserWindow;
|
||||
protected splashWindow?: BrowserWindow;
|
||||
protected windowState?: windowStateKeeper.State;
|
||||
|
||||
@observable activeClusterId: ClusterId;
|
||||
@observable activeClusterId?: ClusterId;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -44,14 +39,12 @@ export class WindowManager extends Singleton {
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private async initMainWindow(showSplash: boolean) {
|
||||
private async initMainWindow(showSplash: boolean): Promise<BrowserWindow> {
|
||||
// Manage main window size and position with state persistence
|
||||
if (!this.windowState) {
|
||||
this.windowState = windowStateKeeper({
|
||||
defaultHeight: 900,
|
||||
defaultWidth: 1440,
|
||||
});
|
||||
}
|
||||
this.windowState ??= windowStateKeeper({
|
||||
defaultHeight: 900,
|
||||
defaultWidth: 1440,
|
||||
});
|
||||
|
||||
if (!this.mainWindow) {
|
||||
// show icon in dock (mac-os only)
|
||||
@@ -88,9 +81,9 @@ export class WindowManager extends Singleton {
|
||||
})
|
||||
.on("closed", () => {
|
||||
// clean up
|
||||
this.windowState.unmanage();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
this.windowState?.unmanage();
|
||||
this.mainWindow = undefined;
|
||||
this.splashWindow = undefined;
|
||||
app.dock?.hide(); // hide icon in dock (mac-os)
|
||||
})
|
||||
.webContents
|
||||
@@ -142,13 +135,17 @@ export class WindowManager extends Singleton {
|
||||
}
|
||||
|
||||
try {
|
||||
if (showSplash) await this.showSplash();
|
||||
if (showSplash) {
|
||||
await this.showSplash();
|
||||
}
|
||||
logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainContentUrl} ...`);
|
||||
await this.mainWindow.loadURL(this.mainContentUrl);
|
||||
} catch (error) {
|
||||
logger.error("Loading main window failed", { error });
|
||||
dialog.showErrorBox("ERROR!", error.toString());
|
||||
dialog.showErrorBox("ERROR!", String(error));
|
||||
}
|
||||
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
protected bindEvents() {
|
||||
@@ -166,7 +163,7 @@ export class WindowManager extends Singleton {
|
||||
viewHasLoaded = new Promise<void>(resolve => {
|
||||
ipcMain.once(bundledExtensionsLoaded, () => resolve());
|
||||
});
|
||||
await this.initMainWindow(showSplash);
|
||||
this.mainWindow = await this.initMainWindow(showSplash);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -181,27 +178,26 @@ export class WindowManager extends Singleton {
|
||||
appEventBus.emit({ name: "app", action: "start" });
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
logger.error(`Showing main window failed: ${error.stack || error}`);
|
||||
dialog.showErrorBox("ERROR!", error.toString());
|
||||
logger.error(`Showing main window failed`, error);
|
||||
dialog.showErrorBox("ERROR!", String(error));
|
||||
}
|
||||
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
sendToView({ channel, frameInfo, data = [] }: SendToViewArgs) {
|
||||
private sendToView(window: BrowserWindow, { channel, frameInfo, data = [] }: SendToViewArgs) {
|
||||
if (frameInfo) {
|
||||
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
|
||||
window.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
|
||||
} else {
|
||||
this.mainWindow.webContents.send(channel, ...data);
|
||||
window.webContents.send(channel, ...data);
|
||||
}
|
||||
}
|
||||
|
||||
async navigateExtension(extId: string, pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||
await this.ensureMainWindow();
|
||||
|
||||
const window = await this.ensureMainWindow();
|
||||
const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId);
|
||||
|
||||
this.sendToView({
|
||||
this.sendToView(window, {
|
||||
channel: "extension:navigate",
|
||||
frameInfo,
|
||||
data: [extId, pageId, params],
|
||||
@@ -209,34 +205,44 @@ export class WindowManager extends Singleton {
|
||||
}
|
||||
|
||||
async navigate(url: string, frameId?: number) {
|
||||
await this.ensureMainWindow();
|
||||
const window = await this.ensureMainWindow();
|
||||
|
||||
this.navigateSync(url, frameId);
|
||||
this.navigateSync(window, url, frameId);
|
||||
}
|
||||
|
||||
navigateSync(url: string, frameId?: number) {
|
||||
navigateSync(window: BrowserWindow, url: string, frameId?: number) {
|
||||
const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId);
|
||||
const channel = frameInfo
|
||||
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
|
||||
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
|
||||
|
||||
this.sendToView({
|
||||
this.sendToView(window, {
|
||||
channel,
|
||||
frameInfo,
|
||||
data: [url],
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
const frameInfo = clusterFrameMap.get(this.activeClusterId);
|
||||
private getActiveClusterFrameInfo() {
|
||||
if (this.activeClusterId) {
|
||||
return clusterFrameMap.get(this.activeClusterId);
|
||||
}
|
||||
|
||||
if (frameInfo) {
|
||||
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
reload() {
|
||||
const frameInfo = this.getActiveClusterFrameInfo();
|
||||
|
||||
if (frameInfo && this.mainWindow) {
|
||||
this.sendToView(this.mainWindow, { channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
||||
} else {
|
||||
webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => {
|
||||
wc.reload();
|
||||
wc.clearHistory();
|
||||
});
|
||||
webContents.getAllWebContents()
|
||||
.filter(wc => wc.getType() === "window")
|
||||
.forEach(wc => {
|
||||
wc.reload();
|
||||
wc.clearHistory();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,23 +269,19 @@ export class WindowManager extends Singleton {
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (isHideable(this.mainWindow)) {
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.hide();
|
||||
}
|
||||
|
||||
if (isHideable(this.splashWindow)) {
|
||||
if (this.splashWindow && !this.splashWindow.isDestroyed()) {
|
||||
this.splashWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.mainWindow.destroy();
|
||||
this.splashWindow.destroy();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
Object.entries(this.disposers).forEach(([name, dispose]) => {
|
||||
dispose();
|
||||
delete this.disposers[name];
|
||||
});
|
||||
this.mainWindow?.destroy();
|
||||
this.splashWindow?.destroy();
|
||||
this.mainWindow = undefined;
|
||||
this.splashWindow = undefined;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user