diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index 5d11dbdf70..504e352ea6 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ClusterContextHandler } from "../context-handler/context-handler"; +import type { ClusterContextHandler, PrometheusDetails } from "../context-handler/context-handler"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import type { Cluster } from "../../common/cluster/cluster"; @@ -13,15 +13,28 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { PrometheusProvider } from "../prometheus/provider"; import { prometheusProviderInjectionToken } from "../prometheus/provider"; import { runInAction } from "mobx"; +import createClusterInjectable from "../create-cluster/create-cluster.injectable"; +import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; +import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; +import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; +import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; +import type { AsyncFnMock } from "@async-fn/jest"; +import type { ReadFile } from "../../common/fs/read-file.injectable"; +import asyncFn from "@async-fn/jest"; +import readFileInjectable from "../../common/fs/read-file.injectable"; +import jsyaml from "js-yaml"; +import type { AsyncResult } from "../../common/utils/async-result"; +import assert from "assert"; enum ServiceResult { Success, Failure, } -const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): PrometheusProvider => ({ +const createTestPrometheusProvider = (kind: string, name: string, alwaysFail: ServiceResult): PrometheusProvider => ({ kind, - name: "TestProvider1", + name, isConfigurable: false, getQuery: () => { throw new Error("getQuery is not implemented."); @@ -41,151 +54,232 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): }, }); -const clusterStub = { - getProxyKubeconfig: () => ({ - makeApiClient: (): void => undefined, - }), - apiUrl: "http://localhost:81", -} as unknown as Cluster; +const proxyConfigValue = jsyaml.dump({ + apiVersion: "v1", + clusters: [{ + cluster: { + server: "https://127.0.0.1:55009", + }, + name: "some-cluster-name", + }], + users: [{ + name: "some-user-name", + user: { + "client-certificate": "/some/path/to/client-cert", + "client-key": "/some/path/to/client-key", + }, + }], + contexts: [{ + name: "some-proxy-context", + context: { + user: "some-user-name", + cluster: "some-cluster-name", + }, + }], + "current-context": "some-proxy-context", +}); describe("ContextHandler", () => { let createContextHandler: (cluster: Cluster) => ClusterContextHandler; let di: DiContainer; + let cluster: Cluster; + let readFileMock: AsyncFnMock; beforeEach(() => { di = getDiForUnitTesting({ doGeneralOverrides: true }); - di.override(createKubeAuthProxyInjectable, () => ({} as any)); + di.override(createKubeAuthProxyInjectable, () => () => ({ + run: async () => {}, + } as Partial as KubeAuthProxy)); + di.override(createKubeconfigManagerInjectable, () => () => ({ + getPath: async () => "/some/proxy-kubeconfig", + } as Partial as KubeconfigManager)); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); + di.override(directoryForTempInjectable, () => "/some-directory-for-temp-data"); + + readFileMock = asyncFn(); + di.override(readFileInjectable, () => readFileMock); createContextHandler = di.inject(createContextHandlerInjectable); + + const createCluster = di.inject(createClusterInjectable); + + cluster = createCluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some/path/to/kubeconfig", + }, { + clusterServerUrl: "http://localhost:81", + }); }); describe("getPrometheusService", () => { - it.each([ - [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 failures = 0; failures < 4; failures += 1) { + describe(`with ${failures} providers that throw`, () => { + beforeEach(() => { + 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}`, `TestService-${i}`, ServiceResult.Failure), + })); + } + }); + }); + + describe("when getting prometheus details", () => { + let details: Promise>; + + beforeEach(() => { + details = createContextHandler(cluster).getPrometheusDetails(); + }); + + it("should read the proxy config file", () => { + expect(readFileMock).toBeCalledWith("/some/proxy-kubeconfig"); + }); + + describe("when reading the proxy config file resolves", () => { + beforeEach(async () => { + await readFileMock.resolveSpecific( + ["/some/proxy-kubeconfig"], + proxyConfigValue, + ); + }); + + it("should resolve with a failed call", async () => { + await expect(details).resolves.toMatchObject({ callWasSuccessful: false }); + }); + }); + }); }); + } - expect(() => createContextHandler(clusterStub).getPrometheusDetails()).rejects.toThrowError(); - }); + for (let successes = 1; successes < 3; successes += 1) { + for (let failures = 0; failures < 4; failures += 1) { + describe(`with ${successes} successful providers followed by ${failures} erroring providers`, () => { + beforeEach(() => { + 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}`, `TestService-${i}`, ServiceResult.Failure), + })); + } - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, 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 < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, `TestService-${i+10}`, 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), - })); - } - }); + describe("when getting prometheus details", () => { + let details: Promise>; - const details = await createContextHandler(clusterStub).getPrometheusDetails(); + beforeEach(() => { + details = createContextHandler(cluster).getPrometheusDetails(); + }); - expect(details.provider.kind === `id_failure_${failures}`); - }); + it("should read the proxy config file", () => { + expect(readFileMock).toBeCalledWith("/some/proxy-kubeconfig"); + }); - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, 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), - })); - } + describe("when reading the proxy config file resolves", () => { + beforeEach(async () => { + await readFileMock.resolveSpecific( + ["/some/proxy-kubeconfig"], + proxyConfigValue, + ); + }); - 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), - })); - } - }); + it("should resolve with the first success provider", async () => { + const result = await details; - const details = await createContextHandler(clusterStub).getPrometheusDetails(); + assert(result.callWasSuccessful); - expect(details.provider.kind === "id_failure_0"); - }); + expect(result.response).toMatchObject({ + provider: { + kind: `id_success_0`, + }, + }); + }); + }); + }); + }); + } + } - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => { - const beforeSuccesses = Math.floor(successes / 2); + for (let successes = 1; successes < 3; successes += 1) { + for (let failures = 0; failures < 4; failures += 1) { + const beforeSuccesses = Math.floor(successes / 2); - 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), - })); - } + describe(`with ${successes} successful providers between by ${failures} erroring providers`, () => { + beforeEach(() => { + 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}`, `TestService-${i}`, ServiceResult.Success), + })); + } - 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) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, `TestService-${i+10}`, ServiceResult.Failure), + })); + } - 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 = beforeSuccesses; i < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, `TestService-${i+20}`, ServiceResult.Success), + })); + } + }); + }); - const details = await createContextHandler(clusterStub).getPrometheusDetails(); + describe("when getting prometheus details", () => { + let details: Promise>; - expect(details.provider.kind === "id_success_0"); - }); + beforeEach(() => { + details = createContextHandler(cluster).getPrometheusDetails(); + }); + + it("should read the proxy config file", () => { + expect(readFileMock).toBeCalledWith("/some/proxy-kubeconfig"); + }); + + describe("when reading the proxy config file resolves", () => { + beforeEach(async () => { + await readFileMock.resolveSpecific( + ["/some/proxy-kubeconfig"], + proxyConfigValue, + ); + }); + + it("should resolve with the first success provider", async () => { + const result = await details; + + assert(result.callWasSuccessful); + + expect(result.response).toMatchObject({ + provider: { + kind: `id_success_0`, + }, + }); + }); + }); + }); + }); + } + } }); }); diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 583c18acc1..6374bef12b 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -9,6 +9,7 @@ import type { Cluster } from "../../common/cluster/cluster"; import type httpProxy from "http-proxy"; import type { UrlWithStringQuery } from "url"; import url from "url"; +import type { KubeConfig } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; @@ -16,6 +17,7 @@ import type { GetPrometheusProviderByKind } from "../prometheus/get-by-kind.inje import type { IComputedValue } from "mobx"; import type { Logger } from "../../common/logger"; import type { MakeApiClient } from "../../common/cluster/make-api-client.injectable"; +import type { AsyncResult } from "../../common/utils/async-result"; export interface PrometheusDetails { prometheusPath: string; @@ -41,7 +43,7 @@ export interface ContextHandlerDependencies { export interface ClusterContextHandler { readonly clusterUrl: UrlWithStringQuery; setupPrometheus(preferences?: ClusterPrometheusPreferences): void; - getPrometheusDetails(): Promise; + getPrometheusDetails(): Promise>; resolveAuthProxyUrl(): Promise; resolveAuthProxyCa(): string; getApiTarget(isLongRunningRequest?: boolean): Promise; @@ -50,6 +52,8 @@ export interface ClusterContextHandler { stopServer(): void; } +const formatPrometheusPath = ({ service, namespace, port }: PrometheusService) => `${namespace}/services/${service}:${port}`; + export class ContextHandler implements ClusterContextHandler { public readonly clusterUrl: UrlWithStringQuery; protected kubeAuthProxy?: KubeAuthProxy; @@ -67,16 +71,20 @@ export class ContextHandler implements ClusterContextHandler { this.prometheus = preferences.prometheus; } - public async getPrometheusDetails(): Promise { - const service = await this.getPrometheusService(); - const prometheusPath = this.ensurePrometheusPath(service); - const provider = this.ensurePrometheusProvider(service); + public async getPrometheusDetails(): Promise> { + const result = await this.getPrometheusService(); - return { prometheusPath, provider }; - } + if (!result.callWasSuccessful) { + return result; + } - protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string { - return `${namespace}/services/${service}:${port}`; + const prometheusPath = formatPrometheusPath(result.response); + const provider = this.ensurePrometheusProvider(result.response); + + return { + callWasSuccessful: true, + response: { prometheusPath, provider }, + }; } protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider { @@ -98,21 +106,45 @@ export class ContextHandler implements ClusterContextHandler { return this.dependencies.prometheusProviders.get(); } - protected async getPrometheusService(): Promise { + private async getProxyKubeconfig(): Promise> { + try { + const proxyConfig = await this.cluster.getProxyKubeconfig(); + + return { + callWasSuccessful: true, + response: proxyConfig, + }; + } catch (error) { + return { + callWasSuccessful: false, + error: error as Error, + }; + } + } + + protected async getPrometheusService(): Promise> { this.setupPrometheus(this.cluster.preferences); if (this.prometheus && this.prometheusProvider) { return { - kind: this.prometheusProvider, - namespace: this.prometheus.namespace, - service: this.prometheus.service, - port: this.prometheus.port, + callWasSuccessful: true, + response: { + kind: this.prometheusProvider, + namespace: this.prometheus.namespace, + service: this.prometheus.service, + port: this.prometheus.port, + }, }; } const providers = this.listPotentialProviders(); - const proxyConfig = await this.cluster.getProxyKubeconfig(); - const apiClient = this.dependencies.makeApiClient(proxyConfig, CoreV1Api); + const proxyConfigResult = await this.getProxyKubeconfig(); + + if (!proxyConfigResult.callWasSuccessful) { + return proxyConfigResult; + } + + const apiClient = this.dependencies.makeApiClient(proxyConfigResult.response, CoreV1Api); const potentialServices = await Promise.allSettled( providers.map(provider => provider.getPrometheusService(apiClient)), ); @@ -126,12 +158,18 @@ export class ContextHandler implements ClusterContextHandler { case "fulfilled": if (res.value) { - return res.value; + return { + callWasSuccessful: true, + response: res.value, + }; } } } - throw new Error("No Prometheus service found", { cause: errors }); + return { + callWasSuccessful: false, + error: new Error("No Prometheus service found", { cause: errors }), + }; } async resolveAuthProxyUrl(): Promise {