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.
*/
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<ReadFile>;
beforeEach(() => {
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);
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<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([
[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<AsyncResult<PrometheusDetails, Error>>;
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<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 { 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<PrometheusDetails>;
getPrometheusDetails(): Promise<AsyncResult<PrometheusDetails, Error>>;
resolveAuthProxyUrl(): Promise<string>;
resolveAuthProxyCa(): string;
getApiTarget(isLongRunningRequest?: boolean): Promise<httpProxy.ServerOptions>;
@ -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<PrometheusDetails> {
const service = await this.getPrometheusService();
const prometheusPath = this.ensurePrometheusPath(service);
const provider = this.ensurePrometheusProvider(service);
public async getPrometheusDetails(): Promise<AsyncResult<PrometheusDetails, Error>> {
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<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);
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<string> {