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

Remove jest-fetch-mock and make fetch injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-08-10 09:04:43 -04:00
parent 5a2a9248e8
commit b1d1b69b3d
13 changed files with 408 additions and 336 deletions

View File

@ -396,7 +396,6 @@
"jest": "^28.1.3",
"jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^28.1.3",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^2.0.9",
"make-plural": "^6.2.2",
"mini-css-extract-plugin": "^2.6.1",

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import fetchInjectable from "./fetch.injectable";
export default getGlobalOverride(fetchInjectable, () => () => {
throw new Error("tried to fetch a resource without override in test");
});

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { RequestInfo, RequestInit, Response } from "node-fetch";
import fetch from "node-fetch";
export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;
const fetchInjectable = getInjectable({
id: "fetch",
instantiate: (): Fetch => fetch,
causesSideEffects: true,
});
export default fetchInjectable;

View File

@ -2,37 +2,40 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { forRemoteCluster, KubeApi } from "../kube-api";
import { KubeApi } from "../kube-api";
import { KubeJsonApi } from "../kube-json-api";
import { KubeObject } from "../kube-object";
import { delay } from "../../utils/delay";
import { PassThrough } from "stream";
import { ApiManager } from "../api-manager";
import type { FetchMock } from "jest-fetch-mock/types";
import type { ApiManager } from "../api-manager";
import { DeploymentApi, Ingress, IngressApi, Pod, PodApi } from "../endpoints";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import apiManagerInjectable from "../api-manager/manager.injectable";
import autoRegistrationInjectable from "../api-manager/auto-registration.injectable";
import type { JsonApiDependencies } from "../json-api";
import loggerInjectable from "../../logger.injectable";
import type { Fetch } from "../../fetch/fetch.injectable";
import fetchInjectable from "../../fetch/fetch.injectable";
import type { CreateKubeApiForRemoteCluster } from "../create-kube-api-for-remote-cluster.injectable";
import createKubeApiForRemoteClusterInjectable from "../create-kube-api-for-remote-cluster.injectable";
import { Headers, Response } from "node-fetch";
import AbortController from "abort-controller";
jest.mock("../api-manager");
const mockFetch = fetch as FetchMock;
describe("forRemoteCluster", () => {
let apiManager: jest.Mocked<ApiManager>;
describe("createKubeApiForRemoteCluster", () => {
let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster;
let fetchMock: jest.MockedFunction<Fetch>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
apiManager = new ApiManager() as jest.Mocked<ApiManager>;
fetchMock = jest.fn();
di.override(fetchInjectable, () => fetchMock);
di.override(apiManagerInjectable, () => apiManager);
createKubeApiForRemoteCluster = di.inject(createKubeApiForRemoteClusterInjectable);
});
it("builds api client for KubeObject", async () => {
const api = forRemoteCluster({
const api = createKubeApiForRemoteCluster({
cluster: {
server: "https://127.0.0.1:6443",
},
@ -45,7 +48,7 @@ describe("forRemoteCluster", () => {
});
it("builds api client for given KubeApi", async () => {
const api = forRemoteCluster({
const api = createKubeApiForRemoteCluster({
cluster: {
server: "https://127.0.0.1:6443",
},
@ -58,7 +61,7 @@ describe("forRemoteCluster", () => {
});
it("calls right api endpoint", async () => {
const api = forRemoteCluster({
const api = createKubeApiForRemoteCluster({
cluster: {
server: "https://127.0.0.1:6443",
},
@ -67,65 +70,42 @@ describe("forRemoteCluster", () => {
},
}, Pod);
mockFetch.mockResponse(async (request: any) => {
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
fetchMock.mockImplementation(async (url) => {
expect(url).toBe("https://127.0.0.1:6443/api/v1/pods");
return {
body: "hello",
};
return new Response("hello");
});
expect.hasAssertions();
await api.list();
expect(await api.list()).toBeNull();
});
});
describe("KubeApi", () => {
let request: KubeJsonApi;
let apiManager: jest.Mocked<ApiManager>;
let registerApiSpy: jest.SpiedFunction<ApiManager["registerApi"]>;
let fetchMock: jest.MockedFunction<Fetch>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
request = new KubeJsonApi({
fetchMock = jest.fn();
di.override(fetchInjectable, () => fetchMock);
const dependencies: JsonApiDependencies = {
logger: di.inject(loggerInjectable),
fetch: di.inject(fetchInjectable),
};
request = new KubeJsonApi(dependencies, {
serverAddress: `http://127.0.0.1:9999`,
apiBase: "/api-kube",
});
apiManager = new ApiManager() as jest.Mocked<ApiManager>;
registerApiSpy = jest.spyOn(di.inject(apiManagerInjectable), "registerApi");
di.override(apiManagerInjectable, () => apiManager);
di.inject(autoRegistrationInjectable);
});
it("uses url from apiBase if apiBase contains the resource", async () => {
mockFetch.mockResponse(async (request: any) => {
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return {
body: JSON.stringify({
resources: [{
name: "ingresses",
}],
}),
};
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
// Even if the old API contains ingresses, KubeApi should prefer the apiBase url
return {
body: JSON.stringify({
resources: [{
name: "ingresses",
}],
}),
};
} else {
return {
body: JSON.stringify({
resources: [],
}),
};
}
});
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new IngressApi({
@ -136,6 +116,46 @@ describe("KubeApi", () => {
checkPreferredVersion: true,
});
fetchMock.mockImplementation(async (url) => {
if (url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return new Response(JSON.stringify({
resources: [{
name: "ingresses",
}],
}));
}
if (url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
return new Response(JSON.stringify({
resources: [{
name: "ingresses",
}],
}));
}
return new Response(JSON.stringify({ resources: [] }));
});
fetchMock.mockImplementation(async (url) => {
if (url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return new Response(JSON.stringify({
resources: [{
name: "ingresses",
}],
}));
}
if (url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
return new Response(JSON.stringify({
resources: [{
name: "ingresses",
}],
}));
}
return new Response(JSON.stringify({ resources: [] }));
});
await kubeApi.get({
name: "foo",
namespace: "default",
@ -145,30 +165,6 @@ describe("KubeApi", () => {
});
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
mockFetch.mockResponse(async (request: any) => {
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return {
body: JSON.stringify({
resources: [],
}),
};
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
return {
body: JSON.stringify({
resources: [{
name: "ingresses",
}],
}),
};
} else {
return {
body: JSON.stringify({
resources: [],
}),
};
}
});
const apiBase = "apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new IngressApi({
@ -179,10 +175,30 @@ describe("KubeApi", () => {
checkPreferredVersion: true,
});
fetchMock.mockImplementation(async (url) => {
if (url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return new Response(JSON.stringify({
resources: [],
}));
}
if (url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
return new Response(JSON.stringify({
resources: [{
name: "ingresses",
}],
}));
}
return new Response(JSON.stringify({ resources: [] }));
});
await kubeApi.get({
name: "foo",
namespace: "default",
});
expect(kubeApi.apiPrefix).toEqual("/apis");
expect(kubeApi.apiGroup).toEqual("extensions");
});
@ -225,7 +241,7 @@ describe("KubeApi", () => {
await (api as any).checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1");
expect(apiManager.registerApi).toBeCalledWith(api);
expect(registerApiSpy).toBeCalledWith(api);
});
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => {
@ -265,7 +281,7 @@ describe("KubeApi", () => {
await (api as any).checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1");
expect(apiManager.registerApi).toBeCalledWith(api);
expect(registerApiSpy).toBeCalledWith(api);
});
});
@ -281,12 +297,12 @@ describe("KubeApi", () => {
it("sends strategic patch by default", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("strategic-merge-patch");
expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("patch");
expect(new Headers(init?.headers).get("content-type")).toMatch("strategic-merge-patch");
expect(init?.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
return {};
return new Response();
});
await api.patch({ name: "test", namespace: "default" }, {
@ -297,12 +313,12 @@ describe("KubeApi", () => {
it("allows to use merge patch", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-patch");
expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("patch");
expect(new Headers(init?.headers).get("content-type")).toMatch("merge-patch");
expect(init?.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
return {};
return new Response();
});
await api.patch({ name: "test", namespace: "default" }, {
@ -313,12 +329,12 @@ describe("KubeApi", () => {
it("allows to use json patch", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("json-patch");
expect(request.body?.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("patch");
expect(new Headers(init?.headers).get("content-type")).toMatch("json-patch");
expect(init?.body?.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
return {};
return new Response();
});
await api.patch({ name: "test", namespace: "default" }, [
@ -329,12 +345,12 @@ describe("KubeApi", () => {
it("allows deep partial patch", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-patch");
expect(request.body?.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("patch");
expect(new Headers(init?.headers).get("content-type")).toMatch("merge-patch");
expect(init?.body?.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
return {};
return new Response();
});
await api.patch(
@ -357,11 +373,11 @@ describe("KubeApi", () => {
it("sends correct request with empty namespace", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background");
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("delete");
expect(url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background");
return {};
return new Response();
});
await api.delete({ name: "foo", namespace: "" });
@ -369,11 +385,11 @@ describe("KubeApi", () => {
it("sends correct request without namespace", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background");
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("delete");
expect(url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background");
return {};
return new Response();
});
await api.delete({ name: "foo" });
@ -381,11 +397,11 @@ describe("KubeApi", () => {
it("sends correct request with namespace", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background");
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("delete");
expect(url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background");
return {};
return new Response();
});
await api.delete({ name: "foo", namespace: "kube-system" });
@ -393,11 +409,11 @@ describe("KubeApi", () => {
it("allows to change propagationPolicy", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE");
expect(request.url).toMatch("propagationPolicy=Orphan");
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("delete");
expect(url).toMatch("propagationPolicy=Orphan");
return {};
return new Response();
});
await api.delete({ name: "foo", namespace: "default", propagationPolicy: "Orphan" });
@ -424,11 +440,8 @@ describe("KubeApi", () => {
it("sends a valid watch request", () => {
const spy = jest.spyOn(request, "getResponse");
mockFetch.mockResponse(async () => {
return {
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
};
fetchMock.mockImplementation(async () => {
return new Response(stream);
});
api.watch({ namespace: "kube-system" });
@ -438,11 +451,8 @@ describe("KubeApi", () => {
it("sends timeout as a query parameter", async () => {
const spy = jest.spyOn(request, "getResponse");
mockFetch.mockResponse(async () => {
return {
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
};
fetchMock.mockImplementation(async () => {
return new Response(stream);
});
api.watch({ namespace: "kube-system", timeout: 60 });
@ -452,15 +462,12 @@ describe("KubeApi", () => {
it("aborts watch using abortController", (done) => {
const spy = jest.spyOn(request, "getResponse");
mockFetch.mockResponse(async request => {
request.signal.addEventListener("abort", () => {
fetchMock.mockImplementation(async (url, init) => {
init?.signal?.addEventListener("abort", () => {
done();
});
return {
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
};
return new Response(stream);
});
const abortController = new AbortController();
@ -490,12 +497,10 @@ describe("KubeApi", () => {
return stream;
});
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
jest.spyOn(global, "fetch").mockImplementation(async () => {
return {
ok: true,
body: stream as never,
} as Partial<Response> as Response;
fetchMock.mockImplementation(async () => {
return new Response(stream, {
status: 200,
});
});
api.watch({
@ -513,11 +518,8 @@ describe("KubeApi", () => {
it("if request not closed after timeout", (done) => {
const spy = jest.spyOn(request, "getResponse");
mockFetch.mockResponse(async () => {
return {
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
};
fetchMock.mockImplementation(async () => {
return new Response(stream);
});
const timeoutSeconds = 1;
@ -550,11 +552,10 @@ describe("KubeApi", () => {
});
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
jest.spyOn(global, "fetch").mockImplementation(async () => {
return {
ok: true,
body: stream as never,
} as Partial<Response> as Response;
fetchMock.mockImplementation(async () => {
return new Response(stream, {
status: 200,
});
});
const timeoutSeconds = 0.5;
@ -591,9 +592,9 @@ describe("KubeApi", () => {
it("should add kind and apiVersion", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("POST");
expect(JSON.parse(String(request.body))).toEqual({
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("post");
expect(JSON.parse(String(init?.body))).toEqual({
kind: "Pod",
apiVersion: "v1",
metadata: {
@ -617,7 +618,7 @@ describe("KubeApi", () => {
},
});
return {};
return new Response();
});
await api.create({
@ -645,9 +646,9 @@ describe("KubeApi", () => {
it("doesn't override metadata.labels", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("POST");
expect(JSON.parse(String(request.body))).toEqual({
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("post");
expect(JSON.parse(String(init?.body))).toEqual({
kind: "Pod",
apiVersion: "v1",
metadata: {
@ -659,7 +660,7 @@ describe("KubeApi", () => {
},
});
return {};
return new Response();
});
await api.create({
@ -688,9 +689,9 @@ describe("KubeApi", () => {
it("doesn't override metadata.labels", async () => {
expect.hasAssertions();
mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PUT");
expect(JSON.parse(String(request.body))).toEqual({
fetchMock.mockImplementation(async (url, init) => {
expect(init?.method).toEqual("put");
expect(JSON.parse(String(init?.body))).toEqual({
metadata: {
name: "foobar",
namespace: "default",
@ -700,7 +701,7 @@ describe("KubeApi", () => {
},
});
return {};
return new Response();
});
await api.update({

View File

@ -0,0 +1,66 @@
/**
* 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 fetchInjectable from "../fetch/fetch.injectable";
import loggerInjectable from "../logger.injectable";
import { apiKubePrefix } from "../vars";
import isDevelopmentInjectable from "../vars/is-development.injectable";
import { apiBaseInjectionToken } from "./api-base";
import type { JsonApiDependencies } from "./json-api";
import type { KubeApiOptions } from "./kube-api";
import { KubeApi } from "./kube-api";
import { KubeJsonApi } from "./kube-json-api";
import type { KubeJsonApiDataFor, KubeObject, KubeObjectConstructor } from "./kube-object";
export interface CreateKubeApiForLocalClusterConfig {
metadata: {
uid: string;
};
}
export interface CreateKubeApiForCluster {
<Object extends KubeObject, Api extends KubeApi<Object>, Data extends KubeJsonApiDataFor<Object>>(
cluster: CreateKubeApiForLocalClusterConfig,
kubeClass: KubeObjectConstructor<Object, Data>,
apiClass: new (apiOpts: KubeApiOptions<Object>) => Api
): Api;
<Object extends KubeObject, Data extends KubeJsonApiDataFor<Object>>(
cluster: CreateKubeApiForLocalClusterConfig,
kubeClass: KubeObjectConstructor<Object, Data>,
apiClass?: new (apiOpts: KubeApiOptions<Object>) => KubeApi<Object>
): KubeApi<Object>;
}
const createKubeApiForClusterInjectable = getInjectable({
id: "create-kube-api-for-cluster",
instantiate: (di): CreateKubeApiForCluster => {
const apiBase = di.inject(apiBaseInjectionToken);
const isDevelopment = di.inject(isDevelopmentInjectable);
const dependencies: JsonApiDependencies = {
fetch: di.inject(fetchInjectable),
logger: di.inject(loggerInjectable),
};
return (cluster: CreateKubeApiForLocalClusterConfig, kubeClass: KubeObjectConstructor<KubeObject, KubeJsonApiDataFor<KubeObject>>, apiClass = KubeApi) => {
const url = new URL(apiBase.config.serverAddress);
const request = new KubeJsonApi(dependencies, {
serverAddress: apiBase.config.serverAddress,
apiBase: apiKubePrefix,
debug: isDevelopment,
}, {
headers: {
"Host": `${cluster.metadata.uid}.localhost:${url.port}`,
},
});
return new apiClass({
objectConstructor: kubeClass,
request,
});
};
},
});
export default createKubeApiForClusterInjectable;

View File

@ -0,0 +1,110 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AgentOptions } from "https";
import { Agent } from "https";
import type { RequestInit } from "node-fetch";
import fetchInjectable from "../fetch/fetch.injectable";
import loggerInjectable from "../logger.injectable";
import isDevelopmentInjectable from "../vars/is-development.injectable";
import type { JsonApiDependencies } from "./json-api";
import type { KubeApiOptions } from "./kube-api";
import { KubeApi } from "./kube-api";
import { KubeJsonApi } from "./kube-json-api";
import type { KubeJsonApiDataFor, KubeObject, KubeObjectConstructor } from "./kube-object";
export interface CreateKubeApiForRemoteClusterConfig {
cluster: {
server: string;
caData?: string;
skipTLSVerify?: boolean;
};
user: {
token?: string | (() => Promise<string>);
clientCertificateData?: string;
clientKeyData?: string;
};
/**
* Custom instance of https.agent to use for the requests
*
* @remarks the custom agent replaced default agent, options skipTLSVerify,
* clientCertificateData, clientKeyData and caData are ignored.
*/
agent?: Agent;
}
export interface CreateKubeApiForRemoteCluster {
<Object extends KubeObject, Api extends KubeApi<Object>, Data extends KubeJsonApiDataFor<Object>>(
config: CreateKubeApiForRemoteClusterConfig,
kubeClass: KubeObjectConstructor<Object, Data>,
apiClass: new (apiOpts: KubeApiOptions<Object>) => Api,
): Api;
<Object extends KubeObject, Data extends KubeJsonApiDataFor<Object>>(
config: CreateKubeApiForRemoteClusterConfig,
kubeClass: KubeObjectConstructor<Object, Data>,
apiClass?: new (apiOpts: KubeApiOptions<Object>) => KubeApi<Object>,
): KubeApi<Object>;
}
const createKubeApiForRemoteClusterInjectable = getInjectable({
id: "create-kube-api-for-remote-cluster",
instantiate: (di): CreateKubeApiForRemoteCluster => {
const isDevelopment = di.inject(isDevelopmentInjectable);
const dependencies: JsonApiDependencies = {
fetch: di.inject(fetchInjectable),
logger: di.inject(loggerInjectable),
};
return (config: CreateKubeApiForRemoteClusterConfig, kubeClass: KubeObjectConstructor<KubeObject, KubeJsonApiDataFor<KubeObject>>, apiClass = KubeApi) => {
const reqInit: RequestInit = {};
const agentOptions: AgentOptions = {};
if (config.cluster.skipTLSVerify === true) {
agentOptions.rejectUnauthorized = false;
}
if (config.user.clientCertificateData) {
agentOptions.cert = config.user.clientCertificateData;
}
if (config.user.clientKeyData) {
agentOptions.key = config.user.clientKeyData;
}
if (config.cluster.caData) {
agentOptions.ca = config.cluster.caData;
}
if (Object.keys(agentOptions).length > 0) {
reqInit.agent = new Agent(agentOptions);
}
if (config.agent) {
reqInit.agent = config.agent;
}
const token = config.user.token;
const request = new KubeJsonApi(dependencies, {
serverAddress: config.cluster.server,
apiBase: "",
debug: isDevelopment,
...(token ? {
getRequestOptions: async () => ({
headers: {
"Authorization": `Bearer ${typeof token === "function" ? await token() : token}`,
},
}),
} : {}),
}, reqInit);
return new apiClass({
objectConstructor: kubeClass,
request,
});
};
},
});
export default createKubeApiForRemoteClusterInjectable;

View File

@ -3,8 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import fetchInjectable from "../fetch/fetch.injectable";
import loggerInjectable from "../logger.injectable";
import { apiKubePrefix, isDebugging } from "../vars";
import { apiBaseInjectionToken } from "./api-base";
import type { JsonApiDependencies } from "./json-api";
import { KubeJsonApi } from "./kube-json-api";
export type CreateKubeJsonApiForCluster = (clusterId: string) => KubeJsonApi;
@ -13,11 +16,15 @@ const createKubeJsonApiForClusterInjectable = getInjectable({
id: "create-kube-json-api-for-cluster",
instantiate: (di): CreateKubeJsonApiForCluster => {
const apiBase = di.inject(apiBaseInjectionToken);
const dependencies: JsonApiDependencies = {
fetch: di.inject(fetchInjectable),
logger: di.inject(loggerInjectable),
};
return (clusterId) => {
const url = new URL(apiBase.config.serverAddress);
return new KubeJsonApi({
return new KubeJsonApi(dependencies, {
serverAddress: apiBase.config.serverAddress,
apiBase: apiKubePrefix,
debug: isDebugging,

View File

@ -9,12 +9,12 @@ import { Agent as HttpAgent } from "http";
import { Agent as HttpsAgent } from "https";
import { merge } from "lodash";
import type { Response, RequestInit } from "node-fetch";
import fetch from "node-fetch";
import { stringify } from "querystring";
import type { Patch } from "rfc6902";
import type { PartialDeep, ValueOf } from "type-fest";
import { EventEmitter } from "../../common/event-emitter";
import logger from "../../common/logger";
import type { Logger } from "../../common/logger";
import type { Fetch } from "../fetch/fetch.injectable";
import type { Defaulted } from "../utils";
import { json } from "../utils";
@ -59,6 +59,11 @@ export type ParamsAndQuery<Params, Query> = (
: Params & { query?: undefined }
);
export interface JsonApiDependencies {
fetch: Fetch;
readonly logger: Logger;
}
export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = JsonApiParams<Data>> {
static readonly reqInitDefault = {
headers: {
@ -71,7 +76,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
debug: false,
};
constructor(public readonly config: JsonApiConfig, reqInit?: RequestInit) {
constructor(protected readonly dependencies: JsonApiDependencies, public readonly config: JsonApiConfig, reqInit?: RequestInit) {
this.config = Object.assign({}, JsonApi.configDefault, config);
this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit);
this.parseResponse = this.parseResponse.bind(this);
@ -105,7 +110,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
}
return fetch(reqUrl, reqInit);
return this.dependencies.fetch(reqUrl, reqInit);
}
get<OutData = Data, Query = QueryParams>(
@ -177,7 +182,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
reqInit,
};
const res = await fetch(reqUrl, reqInit);
const res = await this.dependencies.fetch(reqUrl, reqInit);
return this.parseResponse<OutData>(res, infoLog);
}
@ -233,7 +238,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
protected writeLog(log: JsonApiLog) {
const { method, reqUrl, ...params } = log;
logger.debug(`[JSON-API] request ${method} ${reqUrl}`, params);
this.dependencies.logger.debug(`[JSON-API] request ${method} ${reqUrl}`, params);
}
}

View File

@ -5,22 +5,18 @@
// Base class for building all kubernetes apis
import { isFunction, merge } from "lodash";
import { merge } from "lodash";
import { stringify } from "querystring";
import { apiKubePrefix, isDevelopment } from "../../common/vars";
import { apiBase, apiKube } from "./index";
import { apiKube } from "./index";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import type { KubeObjectConstructor, KubeJsonApiDataFor, KubeObjectMetadata } from "./kube-object";
import { KubeObject, KubeStatus, isKubeStatusData } from "./kube-object";
import byline from "byline";
import type { IKubeWatchEvent } from "./kube-watch-event";
import type { KubeJsonApiData } from "./kube-json-api";
import { KubeJsonApi } from "./kube-json-api";
import type { KubeJsonApiData, KubeJsonApi } from "./kube-json-api";
import type { Disposer } from "../utils";
import { isDefined, noop, WrappedAbortController } from "../utils";
import type { RequestInit } from "node-fetch";
import type { AgentOptions } from "https";
import { Agent } from "https";
import type { Patch } from "rfc6902";
import assert from "assert";
import type { PartialDeep } from "type-fest";
@ -145,136 +141,8 @@ export interface KubeApiResourceList {
resources: KubeApiResource[];
}
export interface ILocalKubeApiConfig {
metadata: {
uid: string;
};
}
export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Background";
/**
* @deprecated
*/
export interface IKubeApiCluster extends ILocalKubeApiConfig { }
export interface IRemoteKubeApiConfig {
cluster: {
server: string;
caData?: string;
skipTLSVerify?: boolean;
};
user: {
token?: string | (() => Promise<string>);
clientCertificateData?: string;
clientKeyData?: string;
};
/**
* Custom instance of https.agent to use for the requests
*
* @remarks the custom agent replaced default agent, options skipTLSVerify,
* clientCertificateData, clientKeyData and caData are ignored.
*/
agent?: Agent;
}
export function forCluster<
Object extends KubeObject,
Api extends KubeApi<Object>,
Data extends KubeJsonApiDataFor<Object>,
>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<Object, Data>, apiClass: new (apiOpts: KubeApiOptions<Object>) => Api): Api;
export function forCluster<
Object extends KubeObject,
Data extends KubeJsonApiDataFor<Object>,
>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<Object, Data>, apiClass?: new (apiOpts: KubeApiOptions<Object>) => KubeApi<Object>): KubeApi<Object>;
export function forCluster<
Object extends KubeObject,
Data extends KubeJsonApiDataFor<Object>,
>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<Object, Data>, apiClass: (new (apiOpts: KubeApiOptions<Object>) => KubeApi<Object>) = KubeApi): KubeApi<Object> {
const url = new URL(apiBase.config.serverAddress);
const request = new KubeJsonApi({
serverAddress: apiBase.config.serverAddress,
apiBase: apiKubePrefix,
debug: isDevelopment,
}, {
headers: {
"Host": `${cluster.metadata.uid}.localhost:${url.port}`,
},
});
return new apiClass({
objectConstructor: kubeClass as KubeObjectConstructor<Object, KubeJsonApiDataFor<Object>>,
request,
});
}
export function forRemoteCluster<
Object extends KubeObject,
Api extends KubeApi<Object>,
Data extends KubeJsonApiDataFor<Object>,
>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<Object, Data>, apiClass: new (apiOpts: KubeApiOptions<Object>) => Api): Api;
export function forRemoteCluster<
Object extends KubeObject,
Data extends KubeJsonApiDataFor<Object>,
>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<Object, Data>, apiClass?: new (apiOpts: KubeApiOptions<Object>) => KubeApi<Object>): KubeApi<Object>;
export function forRemoteCluster<
Object extends KubeObject,
Api extends KubeApi<Object>,
Data extends KubeJsonApiDataFor<Object>,
>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<Object, Data>, apiClass: new (apiOpts: KubeApiOptions<Object>) => KubeApi<Object> = KubeApi): KubeApi<Object> {
const reqInit: RequestInit = {};
const agentOptions: AgentOptions = {};
if (config.cluster.skipTLSVerify === true) {
agentOptions.rejectUnauthorized = false;
}
if (config.user.clientCertificateData) {
agentOptions.cert = config.user.clientCertificateData;
}
if (config.user.clientKeyData) {
agentOptions.key = config.user.clientKeyData;
}
if (config.cluster.caData) {
agentOptions.ca = config.cluster.caData;
}
if (Object.keys(agentOptions).length > 0) {
reqInit.agent = new Agent(agentOptions);
}
if (config.agent) {
reqInit.agent = config.agent;
}
const token = config.user.token;
const request = new KubeJsonApi({
serverAddress: config.cluster.server,
apiBase: "",
debug: isDevelopment,
...(token ? {
getRequestOptions: async () => ({
headers: {
"Authorization": `Bearer ${isFunction(token) ? await token() : token}`,
},
}),
} : {}),
}, reqInit);
if (!apiClass) {
apiClass = KubeApi as new (apiOpts: KubeApiOptions<Object>) => Api;
}
return new apiClass({
objectConstructor: kubeClass as KubeObjectConstructor<Object, KubeJsonApiDataFor<Object>>,
request,
});
}
export type KubeApiWatchCallback<T extends KubeJsonApiData = KubeJsonApiData> = (data: IKubeWatchEvent<T>, error: any) => void;
export interface KubeApiWatchOptions<Object extends KubeObject, Data extends KubeJsonApiDataFor<Object>> {

View File

@ -9,18 +9,28 @@
export { ResourceStack } from "../../common/k8s/resource-stack";
import apiManagerInjectable from "../../common/k8s-api/api-manager/manager.injectable";
import createKubeApiForClusterInjectable from "../../common/k8s-api/create-kube-api-for-cluster.injectable";
import createKubeApiForRemoteClusterInjectable from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
export const apiManager = asLegacyGlobalForExtensionApi(apiManagerInjectable);
export const forCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForClusterInjectable);
export const forRemoteCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForRemoteClusterInjectable);
export {
KubeApi,
forCluster,
forRemoteCluster,
type ILocalKubeApiConfig,
type IRemoteKubeApiConfig,
type IKubeApiCluster,
} from "../../common/k8s-api/kube-api";
export { KubeApi } from "../../common/k8s-api/kube-api";
/**
* @deprecated This type is unused
*/
export interface IKubeApiCluster {
metadata: {
uid: string;
};
}
export type { CreateKubeApiForRemoteClusterConfig as IRemoteKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable";
export type { CreateKubeApiForLocalClusterConfig as ILocalKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-cluster.injectable";
export {
KubeObject,

View File

@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import fetchMock from "jest-fetch-mock";
import configurePackages from "./common/configure-packages";
import { configure } from "mobx";
import { setImmediate } from "timers";
@ -20,9 +19,6 @@ configure({
safeDescriptors: false,
});
// rewire global.fetch to call 'fetchMock'
fetchMock.enableMocks();
// Mock __non_webpack_require__ for tests
globalThis.__non_webpack_require__ = jest.fn();

View File

@ -65,6 +65,7 @@ import forceUpdateModalRootFrameComponentInjectable from "./application-update/f
import legacyOnChannelListenInjectable from "./ipc/legacy-channel-listen.injectable";
import getEntitySettingCommandsInjectable from "./components/command-palette/registered-commands/get-entity-setting-commands.injectable";
import storageSaveDelayInjectable from "./utils/create-storage/storage-save-delay.injectable";
import environmentVariablesInjectable from "../common/utils/environment-variables.injectable";
import type { GlobalOverride } from "../common/test-utils/get-global-override";
import type { PartialDeep } from "type-fest";
@ -164,6 +165,7 @@ export const getDiForUnitTesting = (
}));
});
di.override(environmentVariablesInjectable, () => ({}));
di.override(watchHistoryStateInjectable, () => () => () => {});
di.override(openAppContextMenuInjectable, () => () => {});
di.override(goBackInjectable, () => () => {});

View File

@ -4516,13 +4516,6 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-fetch@^3.0.4:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
node-fetch "2.6.7"
cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -7748,14 +7741,6 @@ jest-environment-node@^28.1.3:
jest-mock "^28.1.3"
jest-util "^28.1.3"
jest-fetch-mock@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
dependencies:
cross-fetch "^3.0.4"
promise-polyfill "^8.1.3"
jest-get-type@^28.0.2:
version "28.0.2"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203"
@ -9341,7 +9326,7 @@ node-addon-api@^5.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501"
integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==
node-fetch@2.6.7, node-fetch@^2.6.7:
node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -10453,11 +10438,6 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
promise-polyfill@^8.1.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6"
integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==
promise-retry@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"