1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix context handler tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-12-02 10:28:21 -05:00
parent cdaba4a4e8
commit 77e529f6b2
2 changed files with 271 additions and 139 deletions

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { getDiForUnitTesting } from "../getDiForUnitTesting";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
@ -13,15 +13,28 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { PrometheusProvider } from "../prometheus/provider"; import type { PrometheusProvider } from "../prometheus/provider";
import { prometheusProviderInjectionToken } from "../prometheus/provider"; import { prometheusProviderInjectionToken } from "../prometheus/provider";
import { runInAction } from "mobx"; 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 { enum ServiceResult {
Success, Success,
Failure, Failure,
} }
const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): PrometheusProvider => ({ const createTestPrometheusProvider = (kind: string, name: string, alwaysFail: ServiceResult): PrometheusProvider => ({
kind, kind,
name: "TestProvider1", name,
isConfigurable: false, isConfigurable: false,
getQuery: () => { getQuery: () => {
throw new Error("getQuery is not implemented."); throw new Error("getQuery is not implemented.");
@ -41,151 +54,232 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult):
}, },
}); });
const clusterStub = { const proxyConfigValue = jsyaml.dump({
getProxyKubeconfig: () => ({ apiVersion: "v1",
makeApiClient: (): void => undefined, clusters: [{
}), cluster: {
apiUrl: "http://localhost:81", server: "https://127.0.0.1:55009",
} as unknown as Cluster; },
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", () => { describe("ContextHandler", () => {
let createContextHandler: (cluster: Cluster) => ClusterContextHandler; let createContextHandler: (cluster: Cluster) => ClusterContextHandler;
let di: DiContainer; let di: DiContainer;
let cluster: Cluster;
let readFileMock: AsyncFnMock<ReadFile>;
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(createKubeAuthProxyInjectable, () => ({} as any)); di.override(createKubeAuthProxyInjectable, () => () => ({
run: async () => {},
} as Partial<KubeAuthProxy> as KubeAuthProxy));
di.override(createKubeconfigManagerInjectable, () => () => ({
getPath: async () => "/some/proxy-kubeconfig",
} as Partial<KubeconfigManager> 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); 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", () => { describe("getPrometheusService", () => {
it.each([ for (let failures = 0; failures < 4; failures += 1) {
[0], describe(`with ${failures} providers that throw`, () => {
[1], beforeEach(() => {
[2], runInAction(() => {
[3], for (let i = 0; i < failures; i += 1) {
])("should throw after %d failure(s)", async (failures) => { di.register(getInjectable({
runInAction(() => { id: `test-prometheus-provider-failure-${i}`,
for (let i = 0; i < failures; i += 1) { injectionToken: prometheusProviderInjectionToken,
di.register(getInjectable({ instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, `TestService-${i}`, ServiceResult.Failure),
id: `test-prometheus-provider-failure-${i}`, }));
injectionToken: prometheusProviderInjectionToken, }
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), });
})); });
}
describe("when getting prometheus details", () => {
let details: Promise<AsyncResult<PrometheusDetails, Error>>;
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([ for (let i = 0; i < successes; i += 1) {
[1, 0], di.register(getInjectable({
[1, 1], id: `test-prometheus-provider-success-${i}`,
[1, 2], injectionToken: prometheusProviderInjectionToken,
[1, 3], instantiate: () => createTestPrometheusProvider(`id_success_${i}`, `TestService-${i+10}`, ServiceResult.Success),
[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) { describe("when getting prometheus details", () => {
di.register(getInjectable({ let details: Promise<AsyncResult<PrometheusDetails, Error>>;
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
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([ describe("when reading the proxy config file resolves", () => {
[1, 0], beforeEach(async () => {
[1, 1], await readFileMock.resolveSpecific(
[1, 2], ["/some/proxy-kubeconfig"],
[1, 3], proxyConfigValue,
[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),
}));
}
for (let i = 0; i < successes; i += 1) { it("should resolve with the first success provider", async () => {
di.register(getInjectable({ const result = await details;
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
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([ for (let successes = 1; successes < 3; successes += 1) {
[1, 0], for (let failures = 0; failures < 4; failures += 1) {
[1, 1], const beforeSuccesses = Math.floor(successes / 2);
[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);
runInAction(() => { describe(`with ${successes} successful providers between by ${failures} erroring providers`, () => {
for (let i = 0; i < beforeSuccesses; i += 1) { beforeEach(() => {
di.register(getInjectable({ runInAction(() => {
id: `test-prometheus-provider-success-${i}`, for (let i = 0; i < beforeSuccesses; i += 1) {
injectionToken: prometheusProviderInjectionToken, di.register(getInjectable({
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), 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) { for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({ di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`, id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken, injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, `TestService-${i+10}`, ServiceResult.Failure),
})); }));
} }
for (let i = beforeSuccesses; i < successes; i += 1) { for (let i = beforeSuccesses; i < successes; i += 1) {
di.register(getInjectable({ di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`, id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken, injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), instantiate: () => createTestPrometheusProvider(`id_success_${i}`, `TestService-${i+20}`, ServiceResult.Success),
})); }));
} }
}); });
});
const details = await createContextHandler(clusterStub).getPrometheusDetails(); describe("when getting prometheus details", () => {
let details: Promise<AsyncResult<PrometheusDetails, Error>>;
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`,
},
});
});
});
});
});
}
}
}); });
}); });

View File

@ -9,6 +9,7 @@ import type { Cluster } from "../../common/cluster/cluster";
import type httpProxy from "http-proxy"; import type httpProxy from "http-proxy";
import type { UrlWithStringQuery } from "url"; import type { UrlWithStringQuery } from "url";
import url from "url"; import url from "url";
import type { KubeConfig } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; 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 { IComputedValue } from "mobx";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import type { MakeApiClient } from "../../common/cluster/make-api-client.injectable"; import type { MakeApiClient } from "../../common/cluster/make-api-client.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
export interface PrometheusDetails { export interface PrometheusDetails {
prometheusPath: string; prometheusPath: string;
@ -41,7 +43,7 @@ export interface ContextHandlerDependencies {
export interface ClusterContextHandler { export interface ClusterContextHandler {
readonly clusterUrl: UrlWithStringQuery; readonly clusterUrl: UrlWithStringQuery;
setupPrometheus(preferences?: ClusterPrometheusPreferences): void; setupPrometheus(preferences?: ClusterPrometheusPreferences): void;
getPrometheusDetails(): Promise<PrometheusDetails>; getPrometheusDetails(): Promise<AsyncResult<PrometheusDetails, Error>>;
resolveAuthProxyUrl(): Promise<string>; resolveAuthProxyUrl(): Promise<string>;
resolveAuthProxyCa(): string; resolveAuthProxyCa(): string;
getApiTarget(isLongRunningRequest?: boolean): Promise<httpProxy.ServerOptions>; getApiTarget(isLongRunningRequest?: boolean): Promise<httpProxy.ServerOptions>;
@ -50,6 +52,8 @@ export interface ClusterContextHandler {
stopServer(): void; stopServer(): void;
} }
const formatPrometheusPath = ({ service, namespace, port }: PrometheusService) => `${namespace}/services/${service}:${port}`;
export class ContextHandler implements ClusterContextHandler { export class ContextHandler implements ClusterContextHandler {
public readonly clusterUrl: UrlWithStringQuery; public readonly clusterUrl: UrlWithStringQuery;
protected kubeAuthProxy?: KubeAuthProxy; protected kubeAuthProxy?: KubeAuthProxy;
@ -67,16 +71,20 @@ export class ContextHandler implements ClusterContextHandler {
this.prometheus = preferences.prometheus; this.prometheus = preferences.prometheus;
} }
public async getPrometheusDetails(): Promise<PrometheusDetails> { public async getPrometheusDetails(): Promise<AsyncResult<PrometheusDetails, Error>> {
const service = await this.getPrometheusService(); const result = await this.getPrometheusService();
const prometheusPath = this.ensurePrometheusPath(service);
const provider = this.ensurePrometheusProvider(service);
return { prometheusPath, provider }; if (!result.callWasSuccessful) {
} return result;
}
protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string { const prometheusPath = formatPrometheusPath(result.response);
return `${namespace}/services/${service}:${port}`; const provider = this.ensurePrometheusProvider(result.response);
return {
callWasSuccessful: true,
response: { prometheusPath, provider },
};
} }
protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider { protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider {
@ -98,21 +106,45 @@ export class ContextHandler implements ClusterContextHandler {
return this.dependencies.prometheusProviders.get(); return this.dependencies.prometheusProviders.get();
} }
protected async getPrometheusService(): Promise<PrometheusService> { private async getProxyKubeconfig(): Promise<AsyncResult<KubeConfig, Error>> {
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<AsyncResult<PrometheusService, Error>> {
this.setupPrometheus(this.cluster.preferences); this.setupPrometheus(this.cluster.preferences);
if (this.prometheus && this.prometheusProvider) { if (this.prometheus && this.prometheusProvider) {
return { return {
kind: this.prometheusProvider, callWasSuccessful: true,
namespace: this.prometheus.namespace, response: {
service: this.prometheus.service, kind: this.prometheusProvider,
port: this.prometheus.port, namespace: this.prometheus.namespace,
service: this.prometheus.service,
port: this.prometheus.port,
},
}; };
} }
const providers = this.listPotentialProviders(); const providers = this.listPotentialProviders();
const proxyConfig = await this.cluster.getProxyKubeconfig(); const proxyConfigResult = await this.getProxyKubeconfig();
const apiClient = this.dependencies.makeApiClient(proxyConfig, CoreV1Api);
if (!proxyConfigResult.callWasSuccessful) {
return proxyConfigResult;
}
const apiClient = this.dependencies.makeApiClient(proxyConfigResult.response, CoreV1Api);
const potentialServices = await Promise.allSettled( const potentialServices = await Promise.allSettled(
providers.map(provider => provider.getPrometheusService(apiClient)), providers.map(provider => provider.getPrometheusService(apiClient)),
); );
@ -126,12 +158,18 @@ export class ContextHandler implements ClusterContextHandler {
case "fulfilled": case "fulfilled":
if (res.value) { 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<string> { async resolveAuthProxyUrl(): Promise<string> {