mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
* Making apiBase injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of Helm functions to be DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make PortForward's use of apiBase fully injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all metric requests to be injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace resource applier with injectables Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch KubeJsonApi.forCluster to be injectable but do not use Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert the rest of shell sessions to be DI-ed - This is a prerequesit for using the new createKubeJsonApiForClusterInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Use new createKubeJsonApiForClusterInjectable for openNodeShellSession Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigDialog injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove jest-fetch-mock and make fetch injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests with new global override Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add new injectable for create KubeJsonApi and JsonApi instances Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix showing-details-for-helm-release behavioural tests - Remove HelmChartStore in favour of all injectables - Create a model for UpgradeChartDockTab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix show details and updating helm releases tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix residual typing issues related to metrics Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash on load due to circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix create resource tab not working Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove legacy apiBase global Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use isDebuggingInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use windowLocationInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove global legacy apiKube Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve injectable filenames compared to the injectables inside Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove modifying input in requestActivePortForwardInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use get(Milli)SecondsFromUnixEpochInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch to non-reactive way of gettting possible helm release versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix typo Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bug in KubeApi constructor Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all KubeApi related tests to use asyncFn Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests after introducing new injectables that have side effects Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bad rebase causing tests to fail Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve expects for multiple field values Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash will looking up api refs Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix breaking change on KubeApi.list Signed-off-by: Sebastian Malton <sebastian@malton.name> * Better fix for formatting urls Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove injectable for time since we should just use useMockTime Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add happy path behavioural tests for upgrade chart tab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove debug message Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix showing-details-for-helm-release tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix installing-helm-chart-from-new-tab tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests relating to hosted cluster id Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots to recent changes in master Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Reupdated upgrade chart new tab test snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness in unit test when using <Animated> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness and improve tests for DeleteClusterDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix kubeconfig-sync tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix <Extensions> tests by removing mockFs and making everything injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix getElectronAppPathInjectable override not returning absolute paths - Also fixes the listing-active-helm-repos-in-prefs tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace all uses of getAbsolutePath with joinPaths as it is more correct and less confusing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix opening application window tests by making override properly absolute Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots relating no longer using getAbsolutePath Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix and add behavioural tests for RenderDelay Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test flakiness because of path side effects, propagate uses to as many places Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension-discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add global override to fix some tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite and fix implementation of KubeconfigManager and its tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests by global override pathExists Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests failing on windows by using injectable verions of path functions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Attempt to fix test timeout by using runInAction Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix setupIpcMainHandlers usage Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
259 lines
7.2 KiB
TypeScript
259 lines
7.2 KiB
TypeScript
/**
|
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
*/
|
|
|
|
// Base http-service / json-api class
|
|
|
|
import { Agent as HttpAgent } from "http";
|
|
import { Agent as HttpsAgent } from "https";
|
|
import { merge } from "lodash";
|
|
import type { Response, RequestInit } 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 type { Logger } from "../../common/logger";
|
|
import type { Fetch } from "../fetch/fetch.injectable";
|
|
import type { Defaulted } from "../utils";
|
|
import { json } from "../utils";
|
|
|
|
export interface JsonApiData {}
|
|
|
|
export interface JsonApiError {
|
|
code?: number;
|
|
message?: string;
|
|
errors?: { id: string; title: string; status?: number }[];
|
|
}
|
|
|
|
export interface JsonApiParams<D> {
|
|
data?: PartialDeep<D>; // request body
|
|
}
|
|
|
|
export interface JsonApiLog {
|
|
method: string;
|
|
reqUrl: string;
|
|
reqInit: RequestInit;
|
|
data?: any;
|
|
error?: any;
|
|
}
|
|
|
|
export type GetRequestOptions = () => Promise<RequestInit>;
|
|
|
|
export interface JsonApiConfig {
|
|
apiBase: string;
|
|
serverAddress: string;
|
|
debug?: boolean;
|
|
getRequestOptions?: GetRequestOptions;
|
|
}
|
|
|
|
const httpAgent = new HttpAgent({ keepAlive: true });
|
|
const httpsAgent = new HttpsAgent({ keepAlive: true });
|
|
|
|
export type QueryParam = string | number | boolean | null | undefined | readonly string[] | readonly number[] | readonly boolean[];
|
|
export type QueryParams = Partial<Record<string, QueryParam | undefined>>;
|
|
|
|
export type ParamsAndQuery<Params, Query> = (
|
|
ValueOf<Query> extends QueryParam
|
|
? Params & { query?: 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: {
|
|
"content-type": "application/json",
|
|
},
|
|
};
|
|
protected readonly reqInit: Defaulted<RequestInit, keyof typeof JsonApi["reqInitDefault"]>;
|
|
|
|
static readonly configDefault: Partial<JsonApiConfig> = {
|
|
debug: false,
|
|
};
|
|
|
|
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);
|
|
this.getRequestOptions = config.getRequestOptions ?? (() => Promise.resolve({}));
|
|
}
|
|
|
|
public readonly onData = new EventEmitter<[Data, Response]>();
|
|
public readonly onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
|
|
private readonly getRequestOptions: GetRequestOptions;
|
|
|
|
async getResponse<Query>(
|
|
path: string,
|
|
params?: ParamsAndQuery<Params, Query>,
|
|
init: RequestInit = {},
|
|
): Promise<Response> {
|
|
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(),
|
|
init,
|
|
);
|
|
const { query } = params ?? {};
|
|
|
|
if (query) {
|
|
const queryString = stringify(query as unknown as QueryParams);
|
|
|
|
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
|
}
|
|
|
|
return this.dependencies.fetch(reqUrl, reqInit);
|
|
}
|
|
|
|
get<OutData = Data, Query = QueryParams>(
|
|
path: string,
|
|
params?: ParamsAndQuery<Params, Query>,
|
|
reqInit: RequestInit = {},
|
|
) {
|
|
return this.request<OutData, Query>(path, params, { ...reqInit, method: "get" });
|
|
}
|
|
|
|
post<OutData = Data, Query = QueryParams>(
|
|
path: string,
|
|
params?: ParamsAndQuery<Params, Query>,
|
|
reqInit: RequestInit = {},
|
|
) {
|
|
return this.request<OutData, Query>(path, params, { ...reqInit, method: "post" });
|
|
}
|
|
|
|
put<OutData = Data, Query = QueryParams>(
|
|
path: string,
|
|
params?: ParamsAndQuery<Params, Query>,
|
|
reqInit: RequestInit = {},
|
|
) {
|
|
return this.request<OutData, Query>(path, params, { ...reqInit, method: "put" });
|
|
}
|
|
|
|
patch<OutData = Data, Query = QueryParams>(
|
|
path: string,
|
|
params?: (ParamsAndQuery<Omit<Params, "data">, Query> & { data?: Patch | PartialDeep<Data> }),
|
|
reqInit: RequestInit = {},
|
|
) {
|
|
return this.request<OutData, Query>(path, params, { ...reqInit, method: "patch" });
|
|
}
|
|
|
|
del<OutData = Data, Query = QueryParams>(
|
|
path: string,
|
|
params?: ParamsAndQuery<Params, Query>,
|
|
reqInit: RequestInit = {},
|
|
) {
|
|
return this.request<OutData, Query>(path, params, { ...reqInit, method: "delete" });
|
|
}
|
|
|
|
protected async request<OutData, Query = QueryParams>(
|
|
path: string,
|
|
params: (ParamsAndQuery<Omit<Params, "data">, Query> & { data?: unknown }) | undefined,
|
|
init: Defaulted<RequestInit, "method">,
|
|
) {
|
|
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
|
|
const reqInit = merge(
|
|
{},
|
|
this.reqInit,
|
|
await this.getRequestOptions(),
|
|
init,
|
|
);
|
|
const { data, query } = params || {};
|
|
|
|
if (data && !reqInit.body) {
|
|
reqInit.body = JSON.stringify(data);
|
|
}
|
|
|
|
if (query) {
|
|
const queryString = stringify(query as unknown as QueryParams);
|
|
|
|
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
|
}
|
|
const infoLog: JsonApiLog = {
|
|
method: reqInit.method.toUpperCase(),
|
|
reqUrl,
|
|
reqInit,
|
|
};
|
|
|
|
const res = await this.dependencies.fetch(reqUrl, reqInit);
|
|
|
|
return this.parseResponse<OutData>(res, infoLog);
|
|
}
|
|
|
|
protected async parseResponse<OutData>(res: Response, log: JsonApiLog): Promise<OutData> {
|
|
const { status } = res;
|
|
|
|
const text = await res.text();
|
|
let data: any;
|
|
|
|
try {
|
|
data = text ? json.parse(text) : ""; // DELETE-requests might not have response-body
|
|
} catch (e) {
|
|
data = text;
|
|
}
|
|
|
|
if (status >= 200 && status < 300) {
|
|
this.onData.emit(data, res);
|
|
this.writeLog({ ...log, data });
|
|
|
|
return data;
|
|
}
|
|
|
|
if (log.method === "GET" && res.status === 403) {
|
|
this.writeLog({ ...log, error: data });
|
|
throw data;
|
|
}
|
|
|
|
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
|
|
|
|
this.onError.emit(error, res);
|
|
this.writeLog({ ...log, error });
|
|
|
|
throw error;
|
|
}
|
|
|
|
protected parseError(error: JsonApiError | string, res: Response): string[] {
|
|
if (typeof error === "string") {
|
|
return [error];
|
|
}
|
|
|
|
if (Array.isArray(error.errors)) {
|
|
return error.errors.map(error => error.title);
|
|
}
|
|
|
|
if (error.message) {
|
|
return [error.message];
|
|
}
|
|
|
|
return [res.statusText || "Error!"];
|
|
}
|
|
|
|
protected writeLog(log: JsonApiLog) {
|
|
const { method, reqUrl, ...params } = log;
|
|
|
|
this.dependencies.logger.debug(`[JSON-API] request ${method} ${reqUrl}`, params);
|
|
}
|
|
}
|
|
|
|
export class JsonApiErrorParsed {
|
|
isUsedForNotification = false;
|
|
|
|
constructor(private error: JsonApiError | DOMException, private messages: string[]) {
|
|
}
|
|
|
|
get isAborted() {
|
|
return this.error.code === DOMException.ABORT_ERR;
|
|
}
|
|
|
|
toString() {
|
|
return this.messages.join("\n");
|
|
}
|
|
}
|