From b1d1b69b3d8ef2958f06ba9935ee42aa0416ba73 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 10 Aug 2022 09:04:43 -0400 Subject: [PATCH] Remove jest-fetch-mock and make fetch injectable Signed-off-by: Sebastian Malton --- package.json | 1 - .../fetch.global-override-for-injectable.ts | 11 + src/common/fetch/fetch.injectable.ts | 17 + src/common/k8s-api/__tests__/kube-api.test.ts | 321 +++++++++--------- .../create-kube-api-for-cluster.injectable.ts | 66 ++++ ...-kube-api-for-remote-cluster.injectable.ts | 110 ++++++ ...te-kube-json-api-for-cluster.injectable.ts | 9 +- src/common/k8s-api/json-api.ts | 17 +- src/common/k8s-api/kube-api.ts | 138 +------- src/extensions/common-api/k8s-api.ts | 26 +- src/jest.setup.ts | 4 - src/renderer/getDiForUnitTesting.tsx | 2 + yarn.lock | 22 +- 13 files changed, 408 insertions(+), 336 deletions(-) create mode 100644 src/common/fetch/fetch.global-override-for-injectable.ts create mode 100644 src/common/fetch/fetch.injectable.ts create mode 100644 src/common/k8s-api/create-kube-api-for-cluster.injectable.ts create mode 100644 src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts diff --git a/package.json b/package.json index 49fa584d7c..9c59041758 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/fetch/fetch.global-override-for-injectable.ts b/src/common/fetch/fetch.global-override-for-injectable.ts new file mode 100644 index 0000000000..cd6160641c --- /dev/null +++ b/src/common/fetch/fetch.global-override-for-injectable.ts @@ -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"); +}); diff --git a/src/common/fetch/fetch.injectable.ts b/src/common/fetch/fetch.injectable.ts new file mode 100644 index 0000000000..c6d2a7e1af --- /dev/null +++ b/src/common/fetch/fetch.injectable.ts @@ -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; + +const fetchInjectable = getInjectable({ + id: "fetch", + instantiate: (): Fetch => fetch, + causesSideEffects: true, +}); + +export default fetchInjectable; diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index 05da1f89d5..9292fab51a 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -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; +describe("createKubeApiForRemoteCluster", () => { + let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster; + let fetchMock: jest.MockedFunction; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - apiManager = new ApiManager() as jest.Mocked; + 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; + let registerApiSpy: jest.SpiedFunction; + let fetchMock: jest.MockedFunction; 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; + 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 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 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({ diff --git a/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts b/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts new file mode 100644 index 0000000000..761c2b3661 --- /dev/null +++ b/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts @@ -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 { + , Data extends KubeJsonApiDataFor>( + cluster: CreateKubeApiForLocalClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass: new (apiOpts: KubeApiOptions) => Api + ): Api; + >( + cluster: CreateKubeApiForLocalClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass?: new (apiOpts: KubeApiOptions) => KubeApi + ): KubeApi; +} + +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>, 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; diff --git a/src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts b/src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts new file mode 100644 index 0000000000..3a3d2204d3 --- /dev/null +++ b/src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts @@ -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); + 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 { + , Data extends KubeJsonApiDataFor>( + config: CreateKubeApiForRemoteClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass: new (apiOpts: KubeApiOptions) => Api, + ): Api; + >( + config: CreateKubeApiForRemoteClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass?: new (apiOpts: KubeApiOptions) => KubeApi, + ): KubeApi; +} + +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>, 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; diff --git a/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts b/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts index 1af9e12fa9..dd4a25be5b 100644 --- a/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts +++ b/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts @@ -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, diff --git a/src/common/k8s-api/json-api.ts b/src/common/k8s-api/json-api.ts index 36b4fdd9f5..c3e07bfa94 100644 --- a/src/common/k8s-api/json-api.ts +++ b/src/common/k8s-api/json-api.ts @@ -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?: undefined } ); +export interface JsonApiDependencies { + fetch: Fetch; + readonly logger: Logger; +} + export class JsonApi = JsonApiParams> { static readonly reqInitDefault = { headers: { @@ -71,7 +76,7 @@ export class JsonApi = 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 = Js reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; } - return fetch(reqUrl, reqInit); + return this.dependencies.fetch(reqUrl, reqInit); } get( @@ -177,7 +182,7 @@ export class JsonApi = Js reqInit, }; - const res = await fetch(reqUrl, reqInit); + const res = await this.dependencies.fetch(reqUrl, reqInit); return this.parseResponse(res, infoLog); } @@ -233,7 +238,7 @@ export class JsonApi = 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); } } diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index ddc2456668..5165a5e130 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -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); - 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, - Data extends KubeJsonApiDataFor, ->(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: new (apiOpts: KubeApiOptions) => Api): Api; -export function forCluster< - Object extends KubeObject, - Data extends KubeJsonApiDataFor, ->(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass?: new (apiOpts: KubeApiOptions) => KubeApi): KubeApi; - -export function forCluster< - Object extends KubeObject, - Data extends KubeJsonApiDataFor, ->(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: (new (apiOpts: KubeApiOptions) => KubeApi) = KubeApi): KubeApi { - 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>, - request, - }); -} - -export function forRemoteCluster< - Object extends KubeObject, - Api extends KubeApi, - Data extends KubeJsonApiDataFor, ->(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: new (apiOpts: KubeApiOptions) => Api): Api; -export function forRemoteCluster< - Object extends KubeObject, - Data extends KubeJsonApiDataFor, ->(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass?: new (apiOpts: KubeApiOptions) => KubeApi): KubeApi; - -export function forRemoteCluster< - Object extends KubeObject, - Api extends KubeApi, - Data extends KubeJsonApiDataFor, ->(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: new (apiOpts: KubeApiOptions) => KubeApi = KubeApi): 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({ - 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) => Api; - } - - return new apiClass({ - objectConstructor: kubeClass as KubeObjectConstructor>, - request, - }); -} - export type KubeApiWatchCallback = (data: IKubeWatchEvent, error: any) => void; export interface KubeApiWatchOptions> { diff --git a/src/extensions/common-api/k8s-api.ts b/src/extensions/common-api/k8s-api.ts index 7fd25b08a1..f5bf1e4953 100644 --- a/src/extensions/common-api/k8s-api.ts +++ b/src/extensions/common-api/k8s-api.ts @@ -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, diff --git a/src/jest.setup.ts b/src/jest.setup.ts index eb1e801f72..41fa87389b 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -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(); diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index aa146ad385..140d0195bf 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -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, () => () => {}); diff --git a/yarn.lock b/yarn.lock index 4b8d99255c..b6177f6a4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"