diff --git a/build/download_binaries.ts b/build/download_binaries.ts index 61c38eae06..826c713fd3 100644 --- a/build/download_binaries.ts +++ b/build/download_binaries.ts @@ -15,8 +15,8 @@ import type { SingleBar } from "cli-progress"; import { MultiBar } from "cli-progress"; import { extract } from "tar-stream"; import gunzip from "gunzip-maybe"; -import { getBinaryName } from "../src/common/vars"; import { isErrnoException, setTimeoutFor } from "../src/common/utils"; +import AbortController from "abort-controller"; type Response = FetchModule.Response; type RequestInfo = FetchModule.RequestInfo; @@ -24,6 +24,14 @@ type RequestInit = FetchModule.RequestInit; const pipeline = promisify(_pipeline); +const getBinaryName = (binaryName: string, { forPlatform }: { forPlatform : string }) => { + if (forPlatform === "windows") { + return `${binaryName}.exe`; + } + + return binaryName; +}; + interface BinaryDownloaderArgs { readonly version: string; readonly platform: SupportedPlatform; diff --git a/src/common/utils/binary-name.injectable.ts b/src/common/utils/binary-name.injectable.ts new file mode 100644 index 0000000000..d240b3aae0 --- /dev/null +++ b/src/common/utils/binary-name.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import normalizedPlatformInjectable from "../vars/normalized-platform.injectable"; + +const binaryNameInjectable = getInjectable({ + id: "binary-name", + instantiate: (di, binaryName) => { + const normalizedPlatform = di.inject(normalizedPlatformInjectable); + + if (normalizedPlatform === "windows") { + return `${binaryName}.exe`; + } + + return binaryName; + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, binaryName: string) => binaryName, + }), +}); + +export default binaryNameInjectable; diff --git a/src/common/utils/bundled-binary-path.injectable.ts b/src/common/utils/bundled-binary-path.injectable.ts new file mode 100644 index 0000000000..42ecbca0d9 --- /dev/null +++ b/src/common/utils/bundled-binary-path.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import joinPathsInjectable from "../path/join-paths.injectable"; +import baseBundledBinariesDirectoryInjectable from "../vars/base-bundled-binaries-dir.injectable"; +import binaryNameInjectable from "./binary-name.injectable"; + +const bundledBinaryPathInjectable = getInjectable({ + id: "bundled-binary-path", + instantiate: (di, name) => { + const joinPaths = di.inject(joinPathsInjectable); + const binaryName = di.inject(binaryNameInjectable, name); + const baseBundledBinariesDirectory = di.inject(baseBundledBinariesDirectoryInjectable); + + return joinPaths(baseBundledBinariesDirectory, binaryName); + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, binaryName: string) => binaryName, + }), +}); + +export default bundledBinaryPathInjectable; diff --git a/src/common/utils/lazy-initialized.ts b/src/common/utils/lazy-initialized.ts deleted file mode 100644 index 62247873fe..0000000000 --- a/src/common/utils/lazy-initialized.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -/** - * A OnceCell is an object that wraps some function that produces a value. - * - * It then only calls the function on the first call to `get()` and returns the - * same instance/value on every subsequent call. - */ -export interface LazyInitialized { - get(): T; -} - -/** - * A function to make a `OnceCell` - */ -export function lazyInitialized(builder: () => T): LazyInitialized { - let value: T | undefined; - let called = false; - - return { - get() { - if (called) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return value!; - } - - value = builder(); - called = true; - - return value; - }, - }; -} diff --git a/src/common/vars.ts b/src/common/vars.ts index 1b0cf1cd2e..fe568b5b86 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -4,9 +4,7 @@ */ // App's common configuration for any process (main, renderer, build pipeline, etc.) -import path from "path"; import type { ThemeId } from "../renderer/themes/store"; -import { lazyInitialized } from "./utils/lazy-initialized"; /** * @deprecated Switch to using isMacInjectable @@ -48,73 +46,6 @@ export const defaultThemeId: ThemeId = "lens-dark"; export const defaultFontSize = 12; export const defaultTerminalFontFamily = "RobotoMono"; export const defaultEditorFontFamily = "RobotoMono"; -/** - * @deprecated use `di.inject(normalizedPlatformInjectable)` instead - */ -export const normalizedPlatform = (() => { - switch (process.platform) { - case "darwin": - return "darwin"; - case "linux": - return "linux"; - case "win32": - return "windows"; - default: - throw new Error(`platform=${process.platform} is unsupported`); - } -})(); -/** - * @deprecated use `di.inject(bundledBinariesNormalizedArchInjectable)` instead - */ -export const normalizedArch = (() => { - switch (process.arch) { - case "arm64": - return "arm64"; - case "x64": - case "amd64": - return "x64"; - case "386": - case "x32": - case "ia32": - return "ia32"; - default: - throw new Error(`arch=${process.arch} is unsupported`); - } -})(); - -export function getBinaryName(name: string, { forPlatform = normalizedPlatform } = {}): string { - if (forPlatform === "windows") { - return `${name}.exe`; - } - - return name; -} - -const resourcesDir = lazyInitialized(() => ( - isProduction - ? process.resourcesPath - : path.join(process.cwd(), "binaries", "client", normalizedPlatform) -)); - -/** - * @deprecated for being explicit side effect. - */ -export const baseBinariesDir = lazyInitialized(() => path.join(resourcesDir.get(), normalizedArch)); - -/** - * @deprecated for being explicit side effect. - */ -export const kubeAuthProxyBinaryName = getBinaryName("lens-k8s-proxy"); - -/** - * @deprecated for being explicit side effect. - */ -export const helmBinaryName = getBinaryName("helm"); - -/** - * @deprecated for being explicit side effect. - */ -export const helmBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), helmBinaryName)); // Apis export const apiPrefix = "/api"; // local router apis diff --git a/src/common/vars/normalized-platform.injectable.ts b/src/common/vars/normalized-platform.injectable.ts index cb34f1d2fa..ee1bf7fb74 100644 --- a/src/common/vars/normalized-platform.injectable.ts +++ b/src/common/vars/normalized-platform.injectable.ts @@ -5,10 +5,12 @@ import { getInjectable } from "@ogre-tools/injectable"; import platformInjectable from "./platform.injectable"; +export type NormalizedPlatform = "darwin" | "linux" | "windows"; + const normalizedPlatformInjectable = getInjectable({ id: "normalized-platform", - instantiate: (di) => { + instantiate: (di): NormalizedPlatform => { const platform = di.inject(platformInjectable); switch (platform) { diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index 92e9d5ae25..5d11dbdf70 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -3,68 +3,43 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { UserStore } from "../../common/user-store"; import type { ClusterContextHandler } from "../context-handler/context-handler"; -import type { PrometheusService, PrometheusProviderRegistry } from "../prometheus"; -import { PrometheusProvider } from "../prometheus"; -import mockFs from "mock-fs"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import type { Cluster } from "../../common/cluster/cluster"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; -import prometheusProviderRegistryInjectable from "../prometheus/prometheus-provider-registry.injectable"; - -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); +import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { PrometheusProvider } from "../prometheus/provider"; +import { prometheusProviderInjectionToken } from "../prometheus/provider"; +import { runInAction } from "mobx"; enum ServiceResult { Success, Failure, - Undefined, } -class TestProvider extends PrometheusProvider { - name = "TestProvider1"; - rateAccuracy = "1h"; - isConfigurable = false; - - constructor(public id: string, public alwaysFail: ServiceResult) { - super(); - } - - getQuery(): string { +const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): PrometheusProvider => ({ + kind, + name: "TestProvider1", + isConfigurable: false, + getQuery: () => { throw new Error("getQuery is not implemented."); - } - - async getPrometheusService(): Promise { - switch (this.alwaysFail) { + }, + getPrometheusService: async () => { + switch (alwaysFail) { case ServiceResult.Success: return { - id: this.id, + kind, namespace: "default", port: 7000, service: "", }; case ServiceResult.Failure: throw new Error("does fail"); - case ServiceResult.Undefined: - return undefined; } - } -} + }, +}); const clusterStub = { getProxyKubeconfig: () => ({ @@ -74,53 +49,34 @@ const clusterStub = { } as unknown as Cluster; describe("ContextHandler", () => { - let createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined; - let prometheusProviderRegistry: PrometheusProviderRegistry; + let createContextHandler: (cluster: Cluster) => ClusterContextHandler; + let di: DiContainer; beforeEach(() => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); - - mockFs({ - "tmp": {}, - }); - + di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(createKubeAuthProxyInjectable, () => ({} as any)); - prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable); - createContextHandler = di.inject(createContextHandlerInjectable); }); - afterEach(() => { - UserStore.resetInstance(); - mockFs.restore(); - }); - describe("getPrometheusService", () => { it.each([ - [0, 0], - [0, 1], - [0, 2], - [0, 3], - ])("should throw from %d success(es) after %d failure(s)", async (successes, failures) => { - let count = 0; + [0], + [1], + [2], + [3], + ])("should throw after %d failure(s)", async (failures) => { + runInAction(() => { + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } + }); - for (let i = 0; i < failures; i += 1) { - const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; - - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); - } - - for (let i = 0; i < successes; i += 1) { - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); - } - - expect(() => { - // TODO: Unit test shouldn't access protected or private methods - const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise }; - - return contextHandler.getPrometheusService(); - }).rejects.toBeDefined(); + expect(() => createContextHandler(clusterStub).getPrometheusDetails()).rejects.toThrowError(); }); it.each([ @@ -133,24 +89,27 @@ describe("ContextHandler", () => { [2, 2], [2, 3], ])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => { - let count = 0; + runInAction(() => { + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } - for (let i = 0; i < failures; i += 1) { - const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; + for (let i = 0; i < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } + }); - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); - } + const details = await createContextHandler(clusterStub).getPrometheusDetails(); - for (let i = 0; i < successes; i += 1) { - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); - } - - // TODO: Unit test shouldn't access protected or private methods - const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise }; - - const service = await contextHandler.getPrometheusService(); - - expect(service.id === `id_${failures}`); + expect(details.provider.kind === `id_failure_${failures}`); }); it.each([ @@ -163,24 +122,27 @@ describe("ContextHandler", () => { [2, 2], [2, 3], ])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => { - let count = 0; + runInAction(() => { + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } - for (let i = 0; i < successes; i += 1) { - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); - } + for (let i = 0; i < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } + }); - for (let i = 0; i < failures; i += 1) { - const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; + const details = await createContextHandler(clusterStub).getPrometheusDetails(); - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); - } - - // TODO: Unit test shouldn't access protected or private methods - const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise }; - - const service = await contextHandler.getPrometheusService(); - - expect(service.id === "id_0"); + expect(details.provider.kind === "id_failure_0"); }); it.each([ @@ -193,45 +155,37 @@ describe("ContextHandler", () => { [2, 2], [2, 3], ])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => { - let count = 0; const beforeSuccesses = Math.floor(successes / 2); - const afterSuccesses = successes - beforeSuccesses; - for (let i = 0; i < beforeSuccesses; i += 1) { - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); - } + runInAction(() => { + for (let i = 0; i < beforeSuccesses; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } - for (let i = 0; i < failures; i += 1) { - const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); - } + for (let i = beforeSuccesses; i < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } + }); - for (let i = 0; i < afterSuccesses; i += 1) { - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); - } + const details = await createContextHandler(clusterStub).getPrometheusDetails(); - // TODO: Unit test shouldn't access protected or private methods - const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise }; - - const service = await contextHandler.getPrometheusService(); - - expect(service.id === "id_0"); - }); - - it("shouldn't pick the second provider of 2 success(es) after 1 failure(s)", async () => { - let count = 0; - - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Failure)); - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); - prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); - - // TODO: Unit test shouldn't access protected or private methods - const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise }; - - const service = await contextHandler.getPrometheusService(); - - expect(service.id).not.toBe("id_2"); + expect(details.provider.kind === "id_success_0"); }); }); }); diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 63a4cfd0b1..7d40bfcd00 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -3,16 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { PrometheusProvider, PrometheusService, PrometheusProviderRegistry } from "../prometheus/provider-registry"; +import type { PrometheusProvider, PrometheusService } from "../prometheus/provider"; import type { ClusterPrometheusPreferences } from "../../common/cluster-types"; import type { Cluster } from "../../common/cluster/cluster"; import type httpProxy from "http-proxy"; import type { UrlWithStringQuery } from "url"; import url from "url"; import { CoreV1Api } from "@kubernetes/client-node"; -import logger from "../logger"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; +import type { GetPrometheusProviderByKind } from "../prometheus/get-by-kind.injectable"; +import type { IComputedValue } from "mobx"; +import type { Logger } from "../../common/logger"; export interface PrometheusDetails { prometheusPath: string; @@ -26,10 +28,12 @@ interface PrometheusServicePreferences { prefix: string; } -interface Dependencies { - readonly createKubeAuthProxy: CreateKubeAuthProxy; +export interface ContextHandlerDependencies { + createKubeAuthProxy: CreateKubeAuthProxy; + getPrometheusProviderByKind: GetPrometheusProviderByKind; readonly authProxyCa: string; - readonly prometheusProviderRegistry: PrometheusProviderRegistry; + readonly prometheusProviders: IComputedValue; + readonly logger: Logger; } export interface ClusterContextHandler { @@ -51,7 +55,7 @@ export class ContextHandler implements ClusterContextHandler { protected prometheusProvider?: string; protected prometheus?: PrometheusServicePreferences; - constructor(private dependencies: Dependencies, protected cluster: Cluster) { + constructor(private readonly dependencies: ContextHandlerDependencies, protected readonly cluster: Cluster) { this.clusterUrl = url.parse(cluster.apiUrl); this.setupPrometheus(cluster.preferences); } @@ -75,22 +79,21 @@ export class ContextHandler implements ClusterContextHandler { protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider { if (!this.prometheusProvider) { - logger.info(`[CONTEXT-HANDLER]: using ${service.id} as prometheus provider for clusterId=${this.cluster.id}`); - this.prometheusProvider = service.id; + this.dependencies.logger.info(`[CONTEXT-HANDLER]: using ${service.kind} as prometheus provider for clusterId=${this.cluster.id}`); + this.prometheusProvider = service.kind; } - return this.dependencies.prometheusProviderRegistry.getByKind(this.prometheusProvider); + return this.dependencies.getPrometheusProviderByKind(this.prometheusProvider); } protected listPotentialProviders(): PrometheusProvider[] { - const registry = this.dependencies.prometheusProviderRegistry; - const provider = this.prometheusProvider && registry.getByKind(this.prometheusProvider); + const provider = this.prometheusProvider && this.dependencies.getPrometheusProviderByKind(this.prometheusProvider); if (provider) { return [provider]; } - return Array.from(registry.providers.values()); + return this.dependencies.prometheusProviders.get(); } protected async getPrometheusService(): Promise { @@ -98,7 +101,7 @@ export class ContextHandler implements ClusterContextHandler { if (this.prometheus && this.prometheusProvider) { return { - id: this.prometheusProvider, + kind: this.prometheusProvider, namespace: this.prometheus.namespace, service: this.prometheus.service, port: this.prometheus.port, diff --git a/src/main/context-handler/create-context-handler.injectable.ts b/src/main/context-handler/create-context-handler.injectable.ts index 8185bd7c80..1567721ac4 100644 --- a/src/main/context-handler/create-context-handler.injectable.ts +++ b/src/main/context-handler/create-context-handler.injectable.ts @@ -3,32 +3,34 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import selfsigned from "selfsigned"; import type { Cluster } from "../../common/cluster/cluster"; -import type { ClusterContextHandler } from "./context-handler"; +import type { ClusterContextHandler, ContextHandlerDependencies } 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"; +import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable"; import URLParse from "url-parse"; -import prometheusProviderRegistryInjectable from "../prometheus/prometheus-provider-registry.injectable"; +import getPrometheusProviderByKindInjectable from "../prometheus/get-by-kind.injectable"; +import prometheusProvidersInjectable from "../prometheus/providers.injectable"; +import loggerInjectable from "../../common/logger.injectable"; const createContextHandlerInjectable = getInjectable({ id: "create-context-handler", instantiate: (di) => { - const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); - const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable); + const dependencies: Omit = { + createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable), + getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable), + prometheusProviders: di.inject(prometheusProvidersInjectable), + logger: di.inject(loggerInjectable), + }; return (cluster: Cluster): ClusterContextHandler => { const clusterUrl = new URLParse(cluster.apiUrl); - const dependencies = { - createKubeAuthProxy, - prometheusProviderRegistry, - authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert, - }; - - return new ContextHandler(dependencies, cluster); + return new ContextHandler({ + ...dependencies, + authProxyCa: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname).cert, + }, cluster); }; }, }); diff --git a/src/main/helm/exec.ts b/src/main/helm/exec.ts index 2847ce6102..6dbaa3f000 100644 --- a/src/main/helm/exec.ts +++ b/src/main/helm/exec.ts @@ -6,9 +6,10 @@ import { promiseExecFile } from "../../common/utils/promise-exec"; import type { ObjectEncodingOptions } from "fs"; import type { ExecFileOptions, ExecFileOptionsWithStringEncoding } from "child_process"; -import { helmBinaryPath } from "../../common/vars"; import { UserStore } from "../../common/user-store"; import { isChildProcessError } from "../../common/utils"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import helmBinaryPathInjectable from "./helm-binary-path.injectable"; /** * ExecFile the bundled helm CLI @@ -19,6 +20,8 @@ export async function execHelm(args: string[], { encoding, ...rest }: ObjectEnco encoding: encoding ?? "utf-8", ...rest, }; + const di = getLegacyGlobalDiForExtensionApi(); + const helmBinaryPath = di.inject(helmBinaryPathInjectable); try { const opts = { ...options }; @@ -29,7 +32,7 @@ export async function execHelm(args: string[], { encoding, ...rest }: ObjectEnco opts.env.HTTPS_PROXY = UserStore.getInstance().httpsProxy; } - const { stdout } = await promiseExecFile(helmBinaryPath.get(), args, opts); + const { stdout } = await promiseExecFile(helmBinaryPath, args, opts); return stdout; } catch (error) { diff --git a/src/main/helm/helm-binary-path.injectable.ts b/src/main/helm/helm-binary-path.injectable.ts index 6d8c661547..19913dfcca 100644 --- a/src/main/helm/helm-binary-path.injectable.ts +++ b/src/main/helm/helm-binary-path.injectable.ts @@ -3,23 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { getBinaryName } from "../../common/vars"; -import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; -import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; -import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import bundledBinaryPathInjectable from "../../common/utils/bundled-binary-path.injectable"; const helmBinaryPathInjectable = getInjectable({ id: "helm-binary-path", - - instantiate: (di) => { - const joinPaths = di.inject(joinPathsInjectable); - const normalizedPlatform = di.inject(normalizedPlatformInjectable); - const baseBundledBinariesDirectory = di.inject(baseBundledBinariesDirectoryInjectable); - - const helmBinaryName = getBinaryName("helm", { forPlatform: normalizedPlatform }); - - return joinPaths(baseBundledBinariesDirectory, helmBinaryName); - }, + instantiate: (di) => di.inject(bundledBinaryPathInjectable, "helm"), }); export default helmBinaryPathInjectable; diff --git a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index f272d9e56f..ba64b69c69 100644 --- a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -6,14 +6,11 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { KubeAuthProxyDependencies } from "./kube-auth-proxy"; import { KubeAuthProxy } from "./kube-auth-proxy"; import type { Cluster } from "../../common/cluster/cluster"; -import selfsigned from "selfsigned"; -import { getBinaryName } from "../../common/vars"; import spawnInjectable from "../child-process/spawn.injectable"; -import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate"; +import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.injectable"; import loggerInjectable from "../../common/logger.injectable"; -import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; -import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import lensK8sProxyPathInjectable from "./lens-k8s-proxy-path.injectable"; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; @@ -21,20 +18,20 @@ const createKubeAuthProxyInjectable = getInjectable({ id: "create-kube-auth-proxy", instantiate: (di): CreateKubeAuthProxy => { - const binaryName = getBinaryName("lens-k8s-proxy"); - const joinPaths = di.inject(joinPathsInjectable); + const dependencies: Omit = { + proxyBinPath: di.inject(lensK8sProxyPathInjectable), + spawn: di.inject(spawnInjectable), + logger: di.inject(loggerInjectable), + waitUntilPortIsUsed: di.inject(waitUntilPortIsUsedInjectable), + }; return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { const clusterUrl = new URL(cluster.apiUrl); - const dependencies: KubeAuthProxyDependencies = { - proxyBinPath: joinPaths(di.inject(baseBundledBinariesDirectoryInjectable), binaryName), - proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate), - spawn: di.inject(spawnInjectable), - logger: di.inject(loggerInjectable), - waitUntilPortIsUsed: di.inject(waitUntilPortIsUsedInjectable), - }; - return new KubeAuthProxy(dependencies, cluster, environmentVariables); + return new KubeAuthProxy({ + ...dependencies, + proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname), + }, cluster, environmentVariables); }; }, }); diff --git a/src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts b/src/main/kube-auth-proxy/kube-auth-proxy-certificate.injectable.ts similarity index 60% rename from src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts rename to src/main/kube-auth-proxy/kube-auth-proxy-certificate.injectable.ts index f6fa71702c..35358f1e52 100644 --- a/src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts +++ b/src/main/kube-auth-proxy/kube-auth-proxy-certificate.injectable.ts @@ -3,15 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type * as selfsigned from "selfsigned"; -import { getOrInsertWith } from "../../common/utils"; +import { generate } from "selfsigned"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -type SelfSignedGenerate = typeof selfsigned.generate; - -const certCache = new Map(); - -export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSignedGenerate): selfsigned.SelfSignedCert { - return getOrInsertWith(certCache, hostname, () => generate( +const kubeAuthProxyCertificateInjectable = getInjectable({ + id: "kube-auth-proxy-certificate", + instantiate: (di, hostname) => generate( [ { name: "commonName", value: "Lens Certificate Authority" }, { name: "organizationName", value: "Lens" }, @@ -31,5 +28,11 @@ export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSign }, ], }, - )); -} + ), + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, hostname: string) => hostname, + }), +}); + +export default kubeAuthProxyCertificateInjectable; + diff --git a/src/main/kube-auth-proxy/lens-k8s-proxy-path.injectable.ts b/src/main/kube-auth-proxy/lens-k8s-proxy-path.injectable.ts new file mode 100644 index 0000000000..cd13bdcf5e --- /dev/null +++ b/src/main/kube-auth-proxy/lens-k8s-proxy-path.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 bundledBinaryPathInjectable from "../../common/utils/bundled-binary-path.injectable"; + +const lensK8sProxyPathInjectable = getInjectable({ + id: "lens-k8s-proxy-path", + instantiate: (di) => di.inject(bundledBinaryPathInjectable, "lens-k8s-proxy"), +}); + +export default lensK8sProxyPathInjectable; diff --git a/src/main/kubectl/binary-name.injectable.ts b/src/main/kubectl/binary-name.injectable.ts index 66b42a6007..f21cabd76e 100644 --- a/src/main/kubectl/binary-name.injectable.ts +++ b/src/main/kubectl/binary-name.injectable.ts @@ -3,17 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; +import binaryNameInjectable from "../../common/utils/binary-name.injectable"; const kubectlBinaryNameInjectable = getInjectable({ id: "kubectl-binary-name", - instantiate: (di) => { - const platform = di.inject(normalizedPlatformInjectable); - - return platform === "windows" - ? "kubectl.exe" - : "kubectl"; - }, + instantiate: (di) => di.inject(binaryNameInjectable, "kubectl"), }); export default kubectlBinaryNameInjectable; diff --git a/src/main/kubectl/bundled-binary-path.injectable.ts b/src/main/kubectl/bundled-binary-path.injectable.ts index f3321f7776..bad89af5f6 100644 --- a/src/main/kubectl/bundled-binary-path.injectable.ts +++ b/src/main/kubectl/bundled-binary-path.injectable.ts @@ -3,20 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import joinPathsInjectable from "../../common/path/join-paths.injectable"; -import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; -import kubectlBinaryNameInjectable from "./binary-name.injectable"; +import bundledBinaryPathInjectable from "../../common/utils/bundled-binary-path.injectable"; const bundledKubectlBinaryPathInjectable = getInjectable({ id: "bundled-kubectl-binary-path", - instantiate: (di) => { - const joinPaths = di.inject(joinPathsInjectable); - - return joinPaths( - di.inject(baseBundledBinariesDirectoryInjectable), - di.inject(kubectlBinaryNameInjectable), - ); - }, + instantiate: (di) => di.inject(bundledBinaryPathInjectable, "kubectl"), }); export default bundledKubectlBinaryPathInjectable; diff --git a/src/main/kubectl/kubectl.ts b/src/main/kubectl/kubectl.ts index b77dbba97d..f0b3c45da0 100644 --- a/src/main/kubectl/kubectl.ts +++ b/src/main/kubectl/kubectl.ts @@ -17,12 +17,13 @@ import { noop } from "lodash/fp"; import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable"; +import type { NormalizedPlatform } from "../../common/vars/normalized-platform.injectable"; const initScriptVersionString = "# lens-initscript v3"; export interface KubectlDependencies { readonly directoryForKubectlBinaries: string; - readonly normalizedDownloadPlatform: "darwin" | "linux" | "windows"; + readonly normalizedDownloadPlatform: NormalizedPlatform; readonly normalizedDownloadArch: "amd64" | "arm64" | "386"; readonly kubectlBinaryName: string; readonly bundledKubectlBinaryPath: string; diff --git a/src/main/prometheus/get-by-kind.injectable.ts b/src/main/prometheus/get-by-kind.injectable.ts new file mode 100644 index 0000000000..93d7567f6e --- /dev/null +++ b/src/main/prometheus/get-by-kind.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { matches } from "lodash/fp"; +import type { PrometheusProvider } from "./provider"; +import prometheusProvidersInjectable from "./providers.injectable"; + +export type GetPrometheusProviderByKind = (kind: string) => PrometheusProvider; + +const getPrometheusProviderByKindInjectable = getInjectable({ + id: "get-prometheus-provider-by-kind", + instantiate: (di): GetPrometheusProviderByKind => { + const providers = di.inject(prometheusProvidersInjectable); + + return (kind) => { + const provider = providers.get().find(matches({ kind })); + + if (!provider) { + throw new Error(`Provider of kind "${kind}" does not exist`); + } + + return provider; + }; + }, +}); + +export default getPrometheusProviderByKindInjectable; diff --git a/src/main/prometheus/helm-14-provider.injectable.ts b/src/main/prometheus/helm-14-provider.injectable.ts new file mode 100644 index 0000000000..6cc66c519e --- /dev/null +++ b/src/main/prometheus/helm-14-provider.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getLensLikeQueryFor } from "./lens-provider.injectable"; +import { createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider"; +import { getInjectable } from "@ogre-tools/injectable"; + +const helm14PrometheusProviderInjectable = getInjectable({ + id: "helm14-prometheus-provider", + instantiate: () => createPrometheusProvider({ + kind: "helm14", + name: "Helm 14.x", + isConfigurable: true, + getQuery: getLensLikeQueryFor({ rateAccuracy: "5m" }), + getService: (client) => findFirstNamespacedService(client, "app=prometheus,component=server,heritage=Helm"), + }), + injectionToken: prometheusProviderInjectionToken, +}); + +export default helm14PrometheusProviderInjectable; + diff --git a/src/main/prometheus/helm-14.ts b/src/main/prometheus/helm-14.ts deleted file mode 100644 index 7ffac4560b..0000000000 --- a/src/main/prometheus/helm-14.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { PrometheusLens } from "./lens"; -import type { CoreV1Api } from "@kubernetes/client-node"; -import type { PrometheusService } from "./provider-registry"; -import { isRequestError } from "../../common/utils"; - -export class PrometheusHelm14 extends PrometheusLens { - readonly id: string = "helm14"; - readonly name: string = "Helm 14.x"; - readonly rateAccuracy: string = "5m"; - readonly isConfigurable: boolean = true; - - public async getPrometheusService(client: CoreV1Api): Promise { - try { - const selector = "app=prometheus,component=server,heritage=Helm"; - const { body: { items: [service] }} = await client.listServiceForAllNamespaces(undefined, undefined, undefined, selector); - - if (service?.metadata?.namespace && service.metadata.name && service.spec?.ports && service.metadata?.labels?.chart?.startsWith("prometheus-14")) { - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port, - }; - } - } catch (error) { - 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`); - } -} diff --git a/src/main/prometheus/helm.ts b/src/main/prometheus/helm-provider.injectable.ts similarity index 78% rename from src/main/prometheus/helm.ts rename to src/main/prometheus/helm-provider.injectable.ts index a2e80cf12e..953bd68277 100644 --- a/src/main/prometheus/helm.ts +++ b/src/main/prometheus/helm-provider.injectable.ts @@ -3,20 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { CoreV1Api } from "@kubernetes/client-node"; -import { inspect } from "util"; -import { PrometheusProvider, type PrometheusService } from "./provider-registry"; -export class PrometheusHelm extends PrometheusProvider { - readonly id: string = "helm"; - readonly name: string = "Helm"; - readonly rateAccuracy: string = "5m"; - readonly isConfigurable: boolean = true; +import type { PrometheusProvider } from "./provider"; +import { createPrometheusProvider, bytesSent, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider"; +import { getInjectable } from "@ogre-tools/injectable"; - public async getPrometheusService(client: CoreV1Api): Promise { - return this.getFirstNamespacedService(client, "app=prometheus,component=server,heritage=Helm"); - } - - public getQuery(opts: Record, queryName: string): string { +export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => ( + (opts, queryName) => { switch(opts.category) { case "cluster": switch (queryName) { @@ -33,7 +25,7 @@ export class PrometheusHelm extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"}) by (component)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; + return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${rateAccuracy}]))`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`; case "cpuLimits": @@ -65,7 +57,7 @@ export class PrometheusHelm extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${rateAccuracy}])) by(node)`; case "cpuCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "cpuAllocatableCapacity": @@ -79,7 +71,7 @@ export class PrometheusHelm extends PrometheusProvider { case "pods": switch (queryName) { case "cpuUsage": - return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; case "cpuLimits": @@ -93,13 +85,13 @@ export class PrometheusHelm extends PrometheusProvider { case "fsUsage": return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; case "fsWrites": - return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "fsReads": - return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkReceive": - return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkTransmit": - return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; } break; case "pvc": @@ -113,17 +105,42 @@ export class PrometheusHelm extends PrometheusProvider { case "ingress": switch (queryName) { case "bytesSentSuccess": - return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^2\\\\d*", + }); case "bytesSentFailure": - return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^5\\\\d*", + }); case "requestDurationSeconds": - return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; case "responseDurationSeconds": - return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; } break; } - throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`); + throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`); } -} +); + +const helmPrometheusProviderInjectable = getInjectable({ + id: "helm-prometheus-provider", + instantiate: () => createPrometheusProvider({ + kind: "helm", + name: "Helm", + isConfigurable: true, + getQuery: getHelmLikeQueryFor({ rateAccuracy: "5m" }), + getService: (client) => findFirstNamespacedService(client, "app=prometheus,component=server,heritage=Helm"), + }), + injectionToken: prometheusProviderInjectionToken, +}); + +export default helmPrometheusProviderInjectable; + diff --git a/src/main/prometheus/index.ts b/src/main/prometheus/index.ts deleted file mode 100644 index 91231ddc43..0000000000 --- a/src/main/prometheus/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -export * from "./provider-registry"; diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens-provider.injectable.ts similarity index 78% rename from src/main/prometheus/lens.ts rename to src/main/prometheus/lens-provider.injectable.ts index e651a0a896..c523c72582 100644 --- a/src/main/prometheus/lens.ts +++ b/src/main/prometheus/lens-provider.injectable.ts @@ -3,22 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { PrometheusService } from "./provider-registry"; -import { PrometheusProvider } from "./provider-registry"; -import type { CoreV1Api } from "@kubernetes/client-node"; -import { inspect } from "util"; +import { bytesSent, prometheusProviderInjectionToken, findNamespacedService, createPrometheusProvider } from "./provider"; +import type { PrometheusProvider } from "./provider"; +import { getInjectable } from "@ogre-tools/injectable"; -export class PrometheusLens extends PrometheusProvider { - readonly id: string = "lens"; - readonly name: string = "Lens"; - readonly rateAccuracy: string = "1m"; - readonly isConfigurable: boolean = false; - - public getPrometheusService(client: CoreV1Api): Promise { - return this.getNamespacedService(client, "prometheus", "lens-metrics"); - } - - public getQuery(opts: Record, queryName: string): string { +export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => ( + (opts, queryName) => { switch(opts.category) { case "cluster": switch (queryName) { @@ -35,7 +25,7 @@ export class PrometheusLens extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"}) by (component)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; + return `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${rateAccuracy}]))`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`; case "cpuLimits": @@ -67,7 +57,7 @@ export class PrometheusLens extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${rateAccuracy}])) by(kubernetes_node)`; case "cpuCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "cpuAllocatableCapacity": @@ -81,7 +71,7 @@ export class PrometheusLens extends PrometheusProvider { case "pods": switch (queryName) { case "cpuUsage": - return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; case "cpuLimits": @@ -95,13 +85,13 @@ export class PrometheusLens extends PrometheusProvider { case "fsUsage": return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; case "fsWrites": - return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "fsReads": - return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkReceive": - return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkTransmit": - return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; } break; case "pvc": @@ -115,17 +105,42 @@ export class PrometheusLens extends PrometheusProvider { case "ingress": switch (queryName) { case "bytesSentSuccess": - return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^2\\\\d*", + }); case "bytesSentFailure": - return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^5\\\\d*", + }); case "requestDurationSeconds": - return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; case "responseDurationSeconds": - return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; } break; } - throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`); + throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`); } -} +); + +const lensPrometheusProviderInjectable = getInjectable({ + id: "lens-prometheus-provider", + instantiate: () => createPrometheusProvider({ + kind: "lens", + name: "Lens", + isConfigurable: false, + getQuery: getLensLikeQueryFor({ rateAccuracy: "1m" }), + getService: (client) => findNamespacedService(client, "prometheus", "lens-metrics"), + }), + injectionToken: prometheusProviderInjectionToken, +}); + +export default lensPrometheusProviderInjectable; + diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator-provider.injectable.ts.ts similarity index 77% rename from src/main/prometheus/operator.ts rename to src/main/prometheus/operator-provider.injectable.ts.ts index 504bb93136..02891d873b 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator-provider.injectable.ts.ts @@ -3,22 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { PrometheusService } from "./provider-registry"; -import { PrometheusProvider } from "./provider-registry"; -import type { CoreV1Api } from "@kubernetes/client-node"; -import { inspect } from "util"; +import type { PrometheusProvider } from "./provider"; +import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider"; +import { getInjectable } from "@ogre-tools/injectable"; -export class PrometheusOperator extends PrometheusProvider { - readonly rateAccuracy: string = "1m"; - readonly id: string = "operator"; - readonly name: string = "Prometheus Operator"; - readonly isConfigurable: boolean = true; - - public async getPrometheusService(client: CoreV1Api): Promise { - return this.getFirstNamespacedService(client, "operated-prometheus=true"); - } - - public getQuery(opts: Record, queryName: string): string { +export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => ( + (opts, queryName) => { switch(opts.category) { case "cluster": switch (queryName) { @@ -35,7 +25,7 @@ export class PrometheusOperator extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`; case "cpuLimits": @@ -67,7 +57,7 @@ export class PrometheusOperator extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; case "cpuCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "cpuAllocatableCapacity": @@ -81,7 +71,7 @@ export class PrometheusOperator extends PrometheusProvider { case "pods": switch (queryName) { case "cpuUsage": - return `sum(rate(container_cpu_usage_seconds_total{container!="", image!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_cpu_usage_seconds_total{container!="", image!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}", resource="cpu", namespace="${opts.namespace}"}) by (${opts.selector})`; case "cpuLimits": @@ -95,13 +85,13 @@ export class PrometheusOperator extends PrometheusProvider { case "fsUsage": return `sum(container_fs_usage_bytes{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}) by (${opts.selector})`; case "fsWrites": - return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "fsReads": - return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkReceive": - return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkTransmit": - return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; } break; case "pvc": @@ -115,17 +105,42 @@ export class PrometheusOperator extends PrometheusProvider { case "ingress": switch (queryName) { case "bytesSentSuccess": - return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^2\\\\d*", + }); case "bytesSentFailure": - return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^5\\\\d*", + }); case "requestDurationSeconds": - return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; case "responseDurationSeconds": - return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; } break; } - throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`); + throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`); } -} +); + +const operatorPrometheusProviderInjectable = getInjectable({ + id: "operator-prometheus-provider", + instantiate: () => createPrometheusProvider({ + kind: "operator", + name: "Prometheus Operator", + isConfigurable: true, + getService: (client) => findFirstNamespacedService(client, "operated-prometheus=true"), + getQuery: getOperatorLikeQueryFor({ rateAccuracy: "1m" }), + }), + injectionToken: prometheusProviderInjectionToken, +}); + +export default operatorPrometheusProviderInjectable; + diff --git a/src/main/prometheus/prometheus-provider-registry.injectable.ts b/src/main/prometheus/prometheus-provider-registry.injectable.ts deleted file mode 100644 index bac9e1a728..0000000000 --- a/src/main/prometheus/prometheus-provider-registry.injectable.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { PrometheusProviderRegistry } from "./provider-registry"; - -const prometheusProviderRegistryInjectable = getInjectable({ - id: "prometheus-provider-registry", - instantiate: () => new PrometheusProviderRegistry(), -}); - -export default prometheusProviderRegistryInjectable; diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts deleted file mode 100644 index c63b22f374..0000000000 --- a/src/main/prometheus/provider-registry.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { CoreV1Api } from "@kubernetes/client-node"; -import { isRequestError } from "../../common/utils"; - -export interface PrometheusService { - id: string; - namespace: string; - service: string; - port: number; -} - -export abstract class PrometheusProvider { - abstract readonly id: string; - abstract readonly name: string; - abstract readonly rateAccuracy: string; - abstract readonly isConfigurable: boolean; - - abstract getQuery(opts: Record, queryName: string): string; - abstract getPrometheusService(client: CoreV1Api): Promise; - - protected bytesSent(ingress: string, namespace: string, statuses: string): string { - return `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; - } - - protected async getFirstNamespacedService(client: CoreV1Api, ...selectors: string[]): Promise { - try { - for (const selector of selectors) { - const { body: { items: [service] }} = await client.listServiceForAllNamespaces(undefined, undefined, undefined, selector); - - if (service?.metadata?.namespace && service.metadata.name && service.spec?.ports) { - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port, - }; - } - } - } catch (error) { - 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`); - } - - protected async getNamespacedService(client: CoreV1Api, name: string, namespace: string): Promise { - try { - 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, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port, - }; - } catch(error) { - throw new Error(`Failed to list services for Prometheus${this.name} in namespace="${namespace}": ${isRequestError(error) ? error.response?.body.message : error}`); - } - } -} - -export class PrometheusProviderRegistry { - public providers = new Map(); - - getByKind(kind: string): PrometheusProvider { - const provider = this.providers.get(kind); - - if (!provider) { - throw new Error("Unknown Prometheus provider"); - } - - return provider; - } - - registerProvider(provider: PrometheusProvider): this { - if (this.providers.has(provider.id)) { - throw new Error("Provider already registered under that kind"); - } - - this.providers.set(provider.id, provider); - - return this; - } -} diff --git a/src/main/prometheus/provider.ts b/src/main/prometheus/provider.ts new file mode 100644 index 0000000000..8944c4c0ed --- /dev/null +++ b/src/main/prometheus/provider.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { CoreV1Api } from "@kubernetes/client-node"; +import { getInjectionToken } from "@ogre-tools/injectable"; +import { isRequestError } from "../../common/utils"; + +export interface PrometheusService extends PrometheusServiceInfo { + kind: string; +} + +export interface PrometheusServiceInfo { + namespace: string; + service: string; + port: number; +} + +export interface PrometheusProvider { + readonly kind: string; + readonly name: string; + readonly isConfigurable: boolean; + + getQuery(opts: Record, queryName: string): string; + getPrometheusService(client: CoreV1Api): Promise; +} + +export interface CreatePrometheusProviderOpts { + readonly kind: string; + readonly name: string; + readonly isConfigurable: boolean; + + getQuery(opts: Record, queryName: string): string; + getService(client: CoreV1Api): Promise; +} + +export const createPrometheusProvider = ({ getService, ...opts }: CreatePrometheusProviderOpts): PrometheusProvider => ({ + ...opts, + getPrometheusService: async (client) => { + try { + return { + kind: opts.kind, + ...await getService(client), + }; + } catch (error) { + throw new Error(`Failed to find Prometheus provider for "${opts.name}"`, { cause: error }); + } + }, +}); + +export async function findFirstNamespacedService(client: CoreV1Api, ...selectors: string[]): Promise { + try { + for (const selector of selectors) { + const { body: { items: [service] }} = await client.listServiceForAllNamespaces(undefined, undefined, undefined, selector); + + if (service?.metadata?.namespace && service.metadata.name && service.spec?.ports) { + return { + namespace: service.metadata.namespace, + service: service.metadata.name, + port: service.spec.ports[0].port, + }; + } + } + } catch (error) { + throw new Error(`Failed to list services in all namespaces: ${isRequestError(error) ? error.response?.body.message : error}`); + } + + throw new Error(`No service found from any namespace`); +} + +export async function findNamespacedService(client: CoreV1Api, name: string, namespace: string): Promise { + try { + const { body: service } = await client.readNamespacedService(name, namespace); + + if (!service.metadata?.namespace || !service.metadata.name || !service.spec?.ports) { + throw new Error(`Service found in namespace="${namespace}" did not have required information`); + } + + return { + namespace: service.metadata.namespace, + service: service.metadata.name, + port: service.spec.ports[0].port, + }; + } catch(error) { + throw new Error(`Failed to list services in namespace="${namespace}": ${isRequestError(error) ? error.response?.body.message : error}`); + } +} + +export interface BytesSentArgs { + rateAccuracy: string; + ingress: string; + namespace: string; + statuses: string; +} + +export function bytesSent({ rateAccuracy, ingress, namespace, statuses }: BytesSentArgs): string { + return `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${rateAccuracy}])) by (ingress, namespace)`; +} + +export const prometheusProviderInjectionToken = getInjectionToken({ + id: "prometheus-provider", +}); diff --git a/src/main/prometheus/providers.injectable.ts b/src/main/prometheus/providers.injectable.ts new file mode 100644 index 0000000000..73fe2dfe50 --- /dev/null +++ b/src/main/prometheus/providers.injectable.ts @@ -0,0 +1,18 @@ +/** + * 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 { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { prometheusProviderInjectionToken } from "./provider"; + +const prometheusProvidersInjectable = getInjectable({ + id: "prometheus-providers", + instantiate: (di) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + + return computedInjectMany(prometheusProviderInjectionToken); + }, +}); + +export default prometheusProvidersInjectable; diff --git a/src/main/prometheus/stacklight.ts b/src/main/prometheus/stacklight-provider.injectable.ts similarity index 77% rename from src/main/prometheus/stacklight.ts rename to src/main/prometheus/stacklight-provider.injectable.ts index 0eee912a24..ca7a946466 100644 --- a/src/main/prometheus/stacklight.ts +++ b/src/main/prometheus/stacklight-provider.injectable.ts @@ -3,22 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { PrometheusService } from "./provider-registry"; -import { PrometheusProvider } from "./provider-registry"; -import type { CoreV1Api } from "@kubernetes/client-node"; -import { inspect } from "util"; +import type { PrometheusProvider } from "./provider"; +import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider"; +import { getInjectable } from "@ogre-tools/injectable"; -export class PrometheusStacklight extends PrometheusProvider { - readonly id: string = "stacklight"; - readonly name: string = "Stacklight"; - readonly rateAccuracy: string = "1m"; - readonly isConfigurable: boolean = true; - - public getPrometheusService(client: CoreV1Api): Promise { - return this.getNamespacedService(client, "prometheus-server", "stacklight"); - } - - public getQuery(opts: Record, queryName: string): string { +export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => ( + (opts, queryName) => { switch(opts.category) { case "cluster": switch (queryName) { @@ -35,7 +25,7 @@ export class PrometheusStacklight extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"}) by (component)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; + return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${rateAccuracy}]))`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`; case "cpuLimits": @@ -67,7 +57,7 @@ export class PrometheusStacklight extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${rateAccuracy}])) by(node)`; case "cpuCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "cpuAllocatableCapacity": @@ -81,7 +71,7 @@ export class PrometheusStacklight extends PrometheusProvider { case "pods": switch (queryName) { case "cpuUsage": - return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; case "cpuLimits": @@ -95,13 +85,13 @@ export class PrometheusStacklight extends PrometheusProvider { case "fsUsage": return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; case "fsWrites": - return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "fsReads": - return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkReceive": - return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; case "networkTransmit": - return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`; } break; case "pvc": @@ -115,17 +105,42 @@ export class PrometheusStacklight extends PrometheusProvider { case "ingress": switch (queryName) { case "bytesSentSuccess": - return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^2\\\\d*", + }); case "bytesSentFailure": - return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"); + return bytesSent({ + rateAccuracy, + ingress: opts.ingress, + namespace: opts.namespace, + statuses: "^5\\\\d*", + }); case "requestDurationSeconds": - return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; case "responseDurationSeconds": - return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (ingress, namespace)`; } break; } - throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`); + throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`); } -} +); + +const stacklightPrometheusProviderInjectable = getInjectable({ + id: "stacklight-prometheus-provider", + instantiate: () => createPrometheusProvider({ + kind: "stacklight", + name: "Stacklight", + isConfigurable: true, + getService: (client) => findFirstNamespacedService(client, "prometheus-server", "stacklight"), + getQuery: getStacklightLikeQueryFor({ rateAccuracy: "1m" }), + }), + injectionToken: prometheusProviderInjectionToken, +}); + +export default stacklightPrometheusProviderInjectable; + diff --git a/src/main/routes/metrics/add-metrics-route.injectable.ts b/src/main/routes/metrics/add-metrics-route.injectable.ts index a11d406737..a2c6a67d50 100644 --- a/src/main/routes/metrics/add-metrics-route.injectable.ts +++ b/src/main/routes/metrics/add-metrics-route.injectable.ts @@ -68,7 +68,7 @@ const addMetricsRouteInjectable = getRouteInjectable({ try { const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails(); - prometheusMetadata.provider = provider?.id; + prometheusMetadata.provider = provider?.kind; prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type; if (!prometheusPath) { diff --git a/src/main/routes/metrics/get-metric-providers-route.injectable.ts b/src/main/routes/metrics/get-metric-providers-route.injectable.ts index 22b2fbf11b..54dfd9bb95 100644 --- a/src/main/routes/metrics/get-metric-providers-route.injectable.ts +++ b/src/main/routes/metrics/get-metric-providers-route.injectable.ts @@ -6,23 +6,22 @@ import { apiPrefix } from "../../../common/vars"; import { getRouteInjectable } from "../../router/router.injectable"; import { route } from "../../router/route"; -import prometheusProviderRegistryInjectable from "../../prometheus/prometheus-provider-registry.injectable"; +import prometheusProvidersInjectable from "../../prometheus/providers.injectable"; const getMetricProvidersRouteInjectable = getRouteInjectable({ id: "get-metric-providers-route", instantiate: (di) => { - const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable); + const prometheusProviders = di.inject(prometheusProvidersInjectable); return route({ method: "get", path: `${apiPrefix}/metrics/providers`, })(() => ({ - response: Array.from( - prometheusProviderRegistry - .providers - .values(), - ({ name, id, isConfigurable }) => ({ name, id, isConfigurable }), + response: ( + prometheusProviders + .get() + .map(({ name, kind: id, isConfigurable }) => ({ name, id, isConfigurable })) ), })); }, diff --git a/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts b/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts deleted file mode 100644 index ef755ac7a1..0000000000 --- a/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { PrometheusLens } from "../../prometheus/lens"; -import { PrometheusHelm } from "../../prometheus/helm"; -import { PrometheusHelm14 } from "../../prometheus/helm-14"; -import { PrometheusOperator } from "../../prometheus/operator"; -import { PrometheusStacklight } from "../../prometheus/stacklight"; -import prometheusProviderRegistryInjectable from "../../prometheus/prometheus-provider-registry.injectable"; -import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; - -const setupPrometheusRegistryInjectable = getInjectable({ - id: "setup-prometheus-registry", - - instantiate: (di) => { - const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable); - - return { - id: "setup-prometheus-registry", - run: () => { - prometheusProviderRegistry - .registerProvider(new PrometheusLens()) - .registerProvider(new PrometheusHelm14()) - .registerProvider(new PrometheusHelm()) - .registerProvider(new PrometheusOperator()) - .registerProvider(new PrometheusStacklight()); - }, - }; - }, - - injectionToken: onLoadOfApplicationInjectionToken, -}); - -export default setupPrometheusRegistryInjectable;