diff --git a/packages/core/src/common/k8s-api/api-base.injectable.ts b/packages/core/src/common/k8s-api/api-base.injectable.ts index d3a914f6a1..3a65912325 100644 --- a/packages/core/src/common/k8s-api/api-base.injectable.ts +++ b/packages/core/src/common/k8s-api/api-base.injectable.ts @@ -3,12 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import lensFetchInjectable from "../fetch/lens-fetch.injectable"; +import lensFetchInjectable from "../../features/lens-fetch/common/lens-fetch.injectable"; import loggerInjectable from "../logger.injectable"; import { apiPrefix } from "../vars"; -import { apiBaseHostHeaderInjectionToken, apiBaseServerAddressInjectionToken } from "./api-base-configs"; +import { apiBaseHostHeaderInjectionToken } from "./api-base-configs"; import isApiBaseInDebugModeInjectable from "./is-api-in-debug-mode.injectable"; -import { JsonApi } from "./json-api"; +import { JsonApi, usingLensFetch } from "./json-api"; const apiBaseInjectable = getInjectable({ id: "api-base", @@ -16,7 +16,7 @@ const apiBaseInjectable = getInjectable({ fetch: di.inject(lensFetchInjectable), logger: di.inject(loggerInjectable), }, { - serverAddress: di.inject(apiBaseServerAddressInjectionToken), + serverAddress: usingLensFetch, apiBase: apiPrefix, debug: di.inject(isApiBaseInDebugModeInjectable), }, { diff --git a/packages/core/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts b/packages/core/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts index dc3a3fef69..1edc6a2817 100644 --- a/packages/core/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts +++ b/packages/core/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts @@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../logger.injectable"; import { apiKubePrefix } from "../vars"; import isDevelopmentInjectable from "../vars/is-development.injectable"; -import apiBaseInjectable from "./api-base.injectable"; +import { apiBaseServerAddressInjectionToken } from "./api-base-configs"; import type { KubeApiConstructor } from "./create-kube-api-for-remote-cluster.injectable"; import createKubeJsonApiInjectable from "./create-kube-json-api.injectable"; import { KubeApi } from "./kube-api"; @@ -34,10 +34,11 @@ export interface CreateKubeApiForCluster { const createKubeApiForClusterInjectable = getInjectable({ id: "create-kube-api-for-cluster", instantiate: (di): CreateKubeApiForCluster => { - const apiBase = di.inject(apiBaseInjectable); const isDevelopment = di.inject(isDevelopmentInjectable); const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); const logger = di.inject(loggerInjectable); + const apiBaseServerAddress = di.inject(apiBaseServerAddressInjectionToken); + const { port } = new URL(apiBaseServerAddress); return ( cluster: CreateKubeApiForLocalClusterConfig, @@ -46,12 +47,12 @@ const createKubeApiForClusterInjectable = getInjectable({ ) => { const request = createKubeJsonApi( { - serverAddress: apiBase.config.serverAddress, + serverAddress: apiBaseServerAddress, apiBase: apiKubePrefix, debug: isDevelopment, }, { headers: { - "Host": `${cluster.metadata.uid}.lens.app:${new URL(apiBase.config.serverAddress).port}`, + "Host": `${cluster.metadata.uid}.lens.app:${port}`, }, }); diff --git a/packages/core/src/common/k8s-api/json-api.ts b/packages/core/src/common/k8s-api/json-api.ts index 00325f3853..c9a26a2dd5 100644 --- a/packages/core/src/common/k8s-api/json-api.ts +++ b/packages/core/src/common/k8s-api/json-api.ts @@ -9,14 +9,14 @@ import { Agent as HttpAgent } from "http"; import { Agent as HttpsAgent } from "https"; import { merge } from "lodash"; import type { Response, RequestInit } from "@k8slens/node-fetch"; -import { stringify } from "querystring"; import type { Patch } from "rfc6902"; -import type { PartialDeep, ValueOf } from "type-fest"; +import type { PartialDeep, SetRequired, ValueOf } from "type-fest"; import { EventEmitter } from "../../common/event-emitter"; import type { Logger } from "../../common/logger"; import type { Fetch } from "../fetch/fetch.injectable"; import type { Defaulted } from "@k8slens/utilities"; -import { isObject, isString, json } from "@k8slens/utilities"; +import { object, isObject, isString, json } from "@k8slens/utilities"; +import { format, parse, URLSearchParams } from "url"; export interface JsonApiData {} @@ -40,13 +40,19 @@ export interface JsonApiLog { export type GetRequestOptions = () => Promise; +export const usingLensFetch = Symbol("using-lens-fetch"); + export interface JsonApiConfig { apiBase: string; - serverAddress: string; + serverAddress: string | typeof usingLensFetch; debug?: boolean; getRequestOptions?: GetRequestOptions; } +export interface InternalJsonApiConfig extends JsonApiConfig { + serverAddress: string | typeof usingLensFetch; +} + const httpAgent = new HttpAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true }); @@ -64,6 +70,11 @@ export interface JsonApiDependencies { readonly logger: Logger; } +interface RequestDetails { + reqUrl: string; + reqInit: SetRequired; +} + export class JsonApi = JsonApiParams> { static readonly reqInitDefault = { headers: { @@ -76,7 +87,7 @@ export class JsonApi = Js debug: false, }; - constructor(protected readonly dependencies: JsonApiDependencies, public readonly config: JsonApiConfig, reqInit?: RequestInit) { + constructor(protected readonly dependencies: JsonApiDependencies, public readonly config: InternalJsonApiConfig, reqInit?: RequestInit) { this.config = Object.assign({}, JsonApi.configDefault, config); this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit); this.parseResponse = this.parseResponse.bind(this); @@ -87,28 +98,53 @@ export class JsonApi = Js public readonly onError = new EventEmitter<[JsonApiErrorParsed, Response]>(); private readonly getRequestOptions: GetRequestOptions; + private async getRequestDetails( + path: string, + query: Partial> | undefined, + init: RequestInit, + ): Promise { + const reqUrl = (() => { + const base = this.config.serverAddress === usingLensFetch + ? parse(`${this.config.apiBase}${path}`) + : parse(`${this.config.serverAddress}${this.config.apiBase}${path}`); + const searchParams = new URLSearchParams(base.query ?? undefined); + + for (const [key, value] of object.entries(query ?? {})) { + searchParams.append(key, value); + } + + return format({ ...base, query: searchParams.toString() }); + })(); + const reqInit = await (async () => { + const baseInit: SetRequired = { method: "get" }; + + if (this.config.serverAddress !== usingLensFetch) { + baseInit.agent = this.config.serverAddress.startsWith("https://") + ? httpsAgent + : httpAgent; + } + + return merge( + baseInit, + this.reqInit, + await this.getRequestOptions(), + init, + ); + })(); + + return { reqInit, reqUrl }; + } + async getResponse( path: string, params?: ParamsAndQuery, init: RequestInit = {}, ): Promise { - let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`; - const reqInit = merge( - { - method: "get", - agent: reqUrl.startsWith("https:") ? httpsAgent : httpAgent, - }, - this.reqInit, - await this.getRequestOptions(), + const { reqInit, reqUrl } = await this.getRequestDetails( + path, + params?.query as Partial>, init, ); - const { query } = params ?? {}; - - if (query && Object.keys(query).length > 0) { - const queryString = stringify(query as unknown as QueryParams); - - reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; - } return this.dependencies.fetch(reqUrl, reqInit); } @@ -155,35 +191,27 @@ export class JsonApi = Js protected async request( path: string, - params: (ParamsAndQuery, Query> & { data?: unknown }) | undefined, + rawParams: (ParamsAndQuery, Query> & { data?: unknown }) | undefined, init: Defaulted, ) { - let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`; - const reqInit = merge( - {}, - this.reqInit, - await this.getRequestOptions(), + const { data, query } = rawParams ?? {}; + const { reqInit, reqUrl } = await this.getRequestDetails( + path, + query as Partial>, init, ); - const { data, query } = params || {}; if (data && !reqInit.body) { reqInit.body = JSON.stringify(data); } - if (query && Object.keys(query).length > 0) { - const queryString = stringify(query as unknown as QueryParams); - - reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; - } + const res = await this.dependencies.fetch(reqUrl, reqInit); const infoLog: JsonApiLog = { method: reqInit.method.toUpperCase(), reqUrl, reqInit, }; - const res = await this.dependencies.fetch(reqUrl, reqInit); - return await this.parseResponse(res, infoLog) as OutData; } diff --git a/packages/core/src/features/lens-fetch/common/lens-fetch-base-url.ts b/packages/core/src/features/lens-fetch/common/lens-fetch-base-url.ts new file mode 100644 index 0000000000..a35f879cc5 --- /dev/null +++ b/packages/core/src/features/lens-fetch/common/lens-fetch-base-url.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; + +export const lensFetchBaseUrlInjectionToken = getInjectionToken({ + id: "lens-fetch-base-url-token", +}); diff --git a/packages/core/src/common/fetch/lens-fetch.global-override-for-injectable.ts b/packages/core/src/features/lens-fetch/common/lens-fetch.global-override-for-injectable.ts similarity index 100% rename from packages/core/src/common/fetch/lens-fetch.global-override-for-injectable.ts rename to packages/core/src/features/lens-fetch/common/lens-fetch.global-override-for-injectable.ts diff --git a/packages/core/src/common/fetch/lens-fetch.injectable.ts b/packages/core/src/features/lens-fetch/common/lens-fetch.injectable.ts similarity index 74% rename from packages/core/src/common/fetch/lens-fetch.injectable.ts rename to packages/core/src/features/lens-fetch/common/lens-fetch.injectable.ts index 634081fda5..a390b18ea6 100644 --- a/packages/core/src/common/fetch/lens-fetch.injectable.ts +++ b/packages/core/src/features/lens-fetch/common/lens-fetch.injectable.ts @@ -5,9 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import { Agent } from "https"; import type { RequestInit, Response } from "@k8slens/node-fetch"; -import lensProxyPortInjectable from "../../main/lens-proxy/lens-proxy-port.injectable"; -import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable"; +import lensProxyCertificateInjectable from "../../../common/certificate/lens-proxy-certificate.injectable"; import fetch from "@k8slens/node-fetch"; +import { lensFetchBaseUrlInjectionToken } from "./lens-fetch-base-url"; export type LensRequestInit = Omit; @@ -16,15 +16,15 @@ export type LensFetch = (pathnameAndQuery: string, init?: LensRequestInit) => Pr const lensFetchInjectable = getInjectable({ id: "lens-fetch", instantiate: (di): LensFetch => { - const lensProxyPort = di.inject(lensProxyPortInjectable); const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); return async (pathnameAndQuery, init = {}) => { const agent = new Agent({ ca: lensProxyCertificate.get().cert, + keepAlive: true, }); - return fetch(`https://127.0.0.1:${lensProxyPort.get()}${pathnameAndQuery}`, { + return fetch(`${di.inject(lensFetchBaseUrlInjectionToken)}${pathnameAndQuery}`, { ...init, agent, }); diff --git a/packages/core/src/features/lens-fetch/main/lens-fetch-base-url.injectable.ts b/packages/core/src/features/lens-fetch/main/lens-fetch-base-url.injectable.ts new file mode 100644 index 0000000000..b535f0ea78 --- /dev/null +++ b/packages/core/src/features/lens-fetch/main/lens-fetch-base-url.injectable.ts @@ -0,0 +1,19 @@ +/** + * 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 lensProxyPortInjectable from "../../../main/lens-proxy/lens-proxy-port.injectable"; +import { lensFetchBaseUrlInjectionToken } from "../common/lens-fetch-base-url"; + +const lensFetchBaseUrlInjectable = getInjectable({ + id: "lens-fetch-base-url", + instantiate: (di) => { + const lensProxyPort = di.inject(lensProxyPortInjectable); + + return `https://127.0.0.1:${lensProxyPort.get()}`; + }, + injectionToken: lensFetchBaseUrlInjectionToken, +}); + +export default lensFetchBaseUrlInjectable; diff --git a/packages/core/src/features/lens-fetch/renderer/lens-fetch-base-url.injectable.ts b/packages/core/src/features/lens-fetch/renderer/lens-fetch-base-url.injectable.ts new file mode 100644 index 0000000000..5bad2c1f20 --- /dev/null +++ b/packages/core/src/features/lens-fetch/renderer/lens-fetch-base-url.injectable.ts @@ -0,0 +1,19 @@ +/** + * 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 windowLocationInjectable from "../../../common/k8s-api/window-location.injectable"; +import { lensFetchBaseUrlInjectionToken } from "../common/lens-fetch-base-url"; + +const lensFetchBaseUrlInjectable = getInjectable({ + id: "lens-fetch-base-url", + instantiate: (di) => { + const { port } = di.inject(windowLocationInjectable); + + return `https://127.0.0.1:${port}`; + }, + injectionToken: lensFetchBaseUrlInjectionToken, +}); + +export default lensFetchBaseUrlInjectable; diff --git a/packages/core/src/main/k8s-request.injectable.ts b/packages/core/src/main/k8s-request.injectable.ts index efa9df1fe7..73d9e86f52 100644 --- a/packages/core/src/main/k8s-request.injectable.ts +++ b/packages/core/src/main/k8s-request.injectable.ts @@ -3,9 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { LensRequestInit } from "../common/fetch/lens-fetch.injectable"; -import lensFetchInjectable from "../common/fetch/lens-fetch.injectable"; import { withTimeout } from "../common/fetch/timeout-controller"; +import type { LensRequestInit } from "../features/lens-fetch/common/lens-fetch.injectable"; +import lensFetchInjectable from "../features/lens-fetch/common/lens-fetch.injectable"; export interface K8sRequestInit extends LensRequestInit { timeout?: number; diff --git a/packages/core/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts b/packages/core/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts index e240388fd8..527fc3a4f2 100644 --- a/packages/core/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts @@ -11,7 +11,7 @@ import { beforeApplicationIsLoadingInjectionToken } from "@k8slens/application"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import initializeBuildVersionInjectable from "../../vars/build-version/init.injectable"; import startLensProxyListeningInjectable from "../../lens-proxy/start-listening.injectable"; -import lensFetchInjectable from "../../../common/fetch/lens-fetch.injectable"; +import lensFetchInjectable from "../../../features/lens-fetch/common/lens-fetch.injectable"; const setupLensProxyInjectable = getInjectable({ id: "setup-lens-proxy", diff --git a/packages/core/src/renderer/k8s/api-kube.injectable.ts b/packages/core/src/renderer/k8s/api-kube.injectable.ts index 9e6bd8c7f8..9a6ddd77e2 100644 --- a/packages/core/src/renderer/k8s/api-kube.injectable.ts +++ b/packages/core/src/renderer/k8s/api-kube.injectable.ts @@ -7,24 +7,27 @@ import assert from "assert"; import { apiKubePrefix } from "../../common/vars"; import { apiKubeInjectionToken } from "../../common/k8s-api/api-kube"; import { storesAndApisCanBeCreatedInjectionToken } from "../../common/k8s-api/stores-apis-can-be-created.token"; -import createKubeJsonApiInjectable from "../../common/k8s-api/create-kube-json-api.injectable"; import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; import showErrorNotificationInjectable from "../components/notifications/show-error-notification.injectable"; import windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; -import { apiBaseServerAddressInjectionToken } from "../../common/k8s-api/api-base-configs"; +import { KubeJsonApi } from "../../common/k8s-api/kube-json-api"; +import lensFetchInjectable from "../../features/lens-fetch/common/lens-fetch.injectable"; +import loggerInjectable from "../../common/logger.injectable"; +import { usingLensFetch } from "../../common/k8s-api/json-api"; const apiKubeInjectable = getInjectable({ id: "api-kube", instantiate: (di) => { assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "apiKube is only available in certain environments"); - const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); - const apiBaseServerAddress = di.inject(apiBaseServerAddressInjectionToken); const isDevelopment = di.inject(isDevelopmentInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const { host } = di.inject(windowLocationInjectable); - const apiKube = createKubeJsonApi({ - serverAddress: apiBaseServerAddress, + const apiKube = new KubeJsonApi({ + fetch: di.inject(lensFetchInjectable), + logger: di.inject(loggerInjectable), + }, { + serverAddress: usingLensFetch, apiBase: apiKubePrefix, debug: isDevelopment, }, { diff --git a/packages/core/src/test-utils/override-internal-backend-routes.injectable.ts b/packages/core/src/test-utils/override-internal-backend-routes.injectable.ts index 3ce83ddcdc..7ea8cac082 100644 --- a/packages/core/src/test-utils/override-internal-backend-routes.injectable.ts +++ b/packages/core/src/test-utils/override-internal-backend-routes.injectable.ts @@ -4,7 +4,7 @@ */ import type { DiContainer } from "@ogre-tools/injectable"; -import lensFetchInjectable from "../common/fetch/lens-fetch.injectable"; +import lensFetchInjectable from "../features/lens-fetch/common/lens-fetch.injectable"; import { Headers, Response } from "@k8slens/node-fetch"; import handleRouteRequestInjectable from "../main/lens-proxy/handle-route-request.injectable"; import fetchInjectable from "../common/fetch/fetch.injectable";