From 47fbf629eac8f4569f4f97cb352e3d5a91ebe192 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 11 Sep 2020 13:36:32 -0400 Subject: [PATCH] remove depricated package request from our code Signed-off-by: Sebastian Malton --- build/download_kubectl.ts | 109 ++++++++------- integration/__tests__/app.tests.ts | 2 +- integration/helpers/utils.ts | 7 +- package.json | 2 + src/common/got-opts.ts | 31 +++++ src/common/request.ts | 29 ---- .../base-cluster-detector.ts | 2 +- src/main/cluster.ts | 44 +++--- src/main/context-handler.ts | 14 +- src/main/helm/helm-repo-manager.ts | 62 ++++----- src/main/kubectl.ts | 59 ++++---- src/main/lens-binary.ts | 114 +++++++--------- src/main/prometheus/provider-registry.ts | 21 +-- src/main/promise-exec.ts | 10 ++ src/main/routes/helm-route.ts | 4 +- src/main/routes/metrics-route.ts | 20 ++- src/renderer/api/endpoints/metrics.api.ts | 4 +- webpack.main.ts | 3 +- yarn.lock | 129 +++++++++++++++++- 19 files changed, 397 insertions(+), 269 deletions(-) create mode 100644 src/common/got-opts.ts delete mode 100644 src/common/request.ts diff --git a/build/download_kubectl.ts b/build/download_kubectl.ts index f046b23e30..fef7b0aca4 100644 --- a/build/download_kubectl.ts +++ b/build/download_kubectl.ts @@ -1,10 +1,16 @@ -import packageInfo from "../package.json"; import fs from "fs"; -import request from "request"; +import fse from "fs-extra"; +import got from "got/dist/source"; import md5File from "md5-file"; -import requestPromise from "request-promise-native"; -import { ensureDir, pathExists } from "fs-extra"; import path from "path"; +import stream from "stream"; +import util, { promisify } from "util"; + +import packageInfo from "../package.json"; +import { GotStreamFunctionOptions } from "../src/common/got-opts"; +import logger from "../src/main/logger"; + +const pipeline = promisify(stream.pipeline); class KubectlDownloader { public kubectlVersion: string; @@ -22,21 +28,26 @@ class KubectlDownloader { } protected async urlEtag() { - const response = await requestPromise({ - method: "HEAD", - uri: this.url, - resolveWithFullResponse: true - }).catch((error) => { console.log(error); }); + try { + const res = await got.head(this.url); + const { etag } = res.headers; - if (response.headers["etag"]) { - return response.headers["etag"].replace(/"/g, ""); + if (Array.isArray(etag)) { + return etag[0].replace(/"/g, ""); + } + + if (typeof etag === "string") { + return etag.replace(/"/g, ""); + } + } catch (err) { + logger.error("Failed to get etag:", err); } return ""; } public async checkBinary() { - const exists = await pathExists(this.path); + const exists = await fse.pathExists(this.path); if (exists) { const hash = md5File.sync(this.path); @@ -55,65 +66,57 @@ class KubectlDownloader { return false; } - public async downloadKubectl() { + public async downloadKubectl(): Promise { const exists = await this.checkBinary(); - if(exists) { - console.log("Already exists and is valid"); - - return; + if (exists) { + return void console.log("Already exists and is valid"); } - await ensureDir(path.dirname(this.path), 0o755); - const file = fs.createWriteStream(this.path); + await fse.ensureDir(path.dirname(this.path), 0o755); console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); - const requestOpts: request.UriOptions & request.CoreOptions = { - uri: this.url, - gzip: true + const options: GotStreamFunctionOptions = { + decompress: true, + isStream: true, }; - const stream = request(requestOpts); - stream.on("complete", () => { - console.log("kubectl binary download finished"); - // eslint-disable-next-line @typescript-eslint/no-empty-function - file.end(() => {}); - }); - - stream.on("error", (error) => { - console.log(error); - // eslint-disable-next-line @typescript-eslint/no-empty-function - fs.unlink(this.path, () => {}); - throw(error); - }); - - return new Promise((resolve, reject) => { - file.on("close", () => { - console.log("kubectl binary download closed"); - fs.chmod(this.path, 0o755, (err) => { - if (err) reject(err); - }); - resolve(); - }); - stream.pipe(file); - }); + try { + // TODO: improve the UI for this to use some sort of loading bar TUI + await pipeline( + got.stream(this.url, options), + fs.createWriteStream(this.path) + ); + await fse.chmod(this.path, 0o755); + } catch (err) { + logger.error("Failed to download kubectl:", err); + } } } const downloadVersion = packageInfo.config.bundledKubectlVersion; const baseDir = path.join(process.env.INIT_CWD, "binaries", "client"); -const downloads = [ + +interface DownloadTarget { + platform: string; + target: string; + arch: string; +} + +const downloads: DownloadTarget[] = [ { platform: "linux", arch: "amd64", target: path.join(baseDir, "linux", "x64", "kubectl") }, { platform: "darwin", arch: "amd64", target: path.join(baseDir, "darwin", "x64", "kubectl") }, { platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", "x64", "kubectl.exe") }, { platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "kubectl.exe") } ]; -downloads.forEach((dlOpts) => { - console.log(dlOpts); - const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target); +async function downloadOne(opts: DownloadTarget) { + const downloader = new KubectlDownloader(downloadVersion, opts.platform, opts.arch, opts.target); - console.log(`Downloading: ${JSON.stringify(dlOpts)}`); - downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete"))); -}); + console.log(`Downloading: ${util.inspect(opts, false, null, true)}`); + await downloader.downloadKubectl(); + await downloader.checkBinary(); + console.log(`Finished downloading for ${opts.platform}/${opts.arch}`); +} +Promise.all(downloads.map(downloadOne)); diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index cf03bccf7c..97a6977f29 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -13,7 +13,7 @@ import * as utils from "../helpers/utils"; import { listHelmRepositories } from "../helpers/utils"; import { fail } from "assert"; -jest.setTimeout(60000); +jest.setTimeout(120000); // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) describe("Lens integration tests", () => { diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index c7fd4e8ddb..a2e91784b7 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -49,12 +49,13 @@ export function describeIf(condition: boolean) { return condition ? describe : describe.skip; } -export function setup(): Application { +export function setup(waitTimeout = 60000): Application { return new Application({ path: AppPaths[process.platform], // path to electron app args: [], startTimeout: 30000, - waitTimeout: 60000, + waitTimeout, + chromeDriverArgs: ["remote-debugging-port=9222"], env: { CICD: "true" } @@ -101,7 +102,7 @@ export async function tearDown(app: Application) { await app.stop(); try { - process.kill(pid, "SIGKILL"); + process.kill(pid, 0); } catch (e) { console.error(e); } diff --git a/package.json b/package.json index b6b1fd1ea5..b4428bed57 100644 --- a/package.json +++ b/package.json @@ -207,7 +207,9 @@ "electron-window-state": "^5.0.3", "filenamify": "^4.1.0", "fs-extra": "^9.0.1", + "got": "^11.8.1", "handlebars": "^4.7.6", + "hpagent": "^0.1.1", "http-proxy": "^1.18.1", "immer": "^8.0.1", "js-yaml": "^3.14.0", diff --git a/src/common/got-opts.ts b/src/common/got-opts.ts new file mode 100644 index 0000000000..55cd20a0ed --- /dev/null +++ b/src/common/got-opts.ts @@ -0,0 +1,31 @@ +import { Options } from "got/dist/source"; +import { HttpsProxyAgent } from "hpagent"; +import { userStore } from "./user-store"; + +declare type Except = Pick>; +declare type Merge = Except> & SecondType; +export type GotStreamFunctionOptions = Merge; + +export function defaultGotOptions(): Options { + const { httpsProxy, allowUntrustedCAs } = userStore.preferences; + const rejectUnauthorized = !allowUntrustedCAs; + + if (!httpsProxy) { + return { rejectUnauthorized }; + } + + const https = new HttpsProxyAgent({ + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + proxy: httpsProxy, + }); + + return { + agent: { https }, + rejectUnauthorized, + }; +} diff --git a/src/common/request.ts b/src/common/request.ts deleted file mode 100644 index ca34f4a961..0000000000 --- a/src/common/request.ts +++ /dev/null @@ -1,29 +0,0 @@ -import request from "request"; -import requestPromise from "request-promise-native"; -import { userStore } from "./user-store"; - -// todo: get rid of "request" (deprecated) -// https://github.com/lensapp/lens/issues/459 - -function getDefaultRequestOpts(): Partial { - const { httpsProxy, allowUntrustedCAs } = userStore.preferences; - - return { - proxy: httpsProxy || undefined, - rejectUnauthorized: !allowUntrustedCAs, - }; -} - -/** - * @deprecated - */ -export function customRequest(opts: request.Options) { - return request.defaults(getDefaultRequestOpts())(opts); -} - -/** - * @deprecated - */ -export function customRequestPromise(opts: requestPromise.Options) { - return requestPromise.defaults(getDefaultRequestOpts())(opts); -} diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts index 885f96c33e..61d3526c85 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster-detectors/base-cluster-detector.ts @@ -26,7 +26,7 @@ export class BaseClusterDetector { timeout: 30000, ...options, headers: { - Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest() + Host: `${this.cluster.id}.${this.cluster.kubeProxyUrl.host}`, // required in ClusterManager.getClusterForRequest() ...(options.headers || {}), }, }); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index eee88cd6db..043e4c5b99 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,6 +1,5 @@ import { ipcMain } from "electron"; import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store"; -import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; @@ -10,12 +9,13 @@ import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttribu import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager"; import { loadConfig, validateKubeConfig } from "../common/kube-helpers"; -import request, { RequestPromiseOptions } from "request-promise-native"; import { apiResources, KubeApiResource } from "../common/rbac"; import logger from "./logger"; import { VersionDetector } from "./cluster-detectors/version-detector"; import { detectorRegistry } from "./cluster-detectors/detector-registry"; import plimit from "p-limit"; +import got, { OptionsOfJSONResponseBody } from "got"; +import { URL } from "url"; export enum ClusterStatus { AccessGranted = 2, @@ -86,7 +86,7 @@ export class Cluster implements ClusterModel, ClusterState { whenReady = when(() => this.ready); /** - * Is cluster object initializinng on-going + * Is cluster object initializing on-going * * @observable */ @@ -128,7 +128,7 @@ export class Cluster implements ClusterModel, ClusterState { * @observable * @internal */ - @observable kubeProxyUrl: string; // lens-proxy to kube-api url + @observable kubeProxyUrl: URL; // lens-proxy to kube-api url /** * Is cluster instance enabled (disabled clusters are currently hidden) * @@ -298,7 +298,7 @@ export class Cluster implements ClusterModel, ClusterState { this.initializing = true; this.contextHandler = new ContextHandler(this); this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port); - this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; + this.kubeProxyUrl = new URL(`http://localhost:${port}${apiKubePrefix}`); this.initialized = true; logger.info(`[CLUSTER]: "${this.contextName}" init success`, { id: this.id, @@ -495,31 +495,31 @@ export class Cluster implements ClusterModel, ClusterState { return this.kubeconfigManager.getPath(); } - protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { - options.headers ??= {}; - options.json ??= true; - options.timeout ??= 30000; - options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest() + private getUrlTo(path: string): URL { + const url = new URL(this.kubeProxyUrl.toString()); - return request(this.kubeProxyUrl + path, options); + url.pathname += path; + + return url; } - /** - * - * @param prometheusPath path to prometheus service - * @param queryParams query parameters - * @internal - */ - getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) { + protected k8sRequest(url: URL, options: OptionsOfJSONResponseBody = {}): Promise { + options.timeout ??= 5000; + options.headers ??= {}; + options.headers["content-type"] ??= "application/json"; + options.headers.host ??= `${this.id}.${this.kubeProxyUrl.host}`; // required in ClusterManager.getClusterForRequest() + + return got(url, options).json(); + } + + getMetrics(prometheusPath: string, searchParams: Record) { const prometheusPrefix = this.preferences.prometheus?.prefix || ""; const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`; - return this.k8sRequest(metricsPath, { + return this.k8sRequest(this.getUrlTo(metricsPath), { timeout: 0, - resolveWithFullResponse: false, - json: true, method: "POST", - form: queryParams, + form: searchParams, }); } diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index e94520b9be..ae2b38f432 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -1,4 +1,4 @@ -import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"; +import type { PrometheusService } from "./prometheus/provider-registry"; import type { ClusterPrometheusPreferences } from "../common/cluster-store"; import type { Cluster } from "./cluster"; import type httpProxy from "http-proxy"; @@ -58,14 +58,14 @@ export class ContextHandler { async getPrometheusService(): Promise { const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; - const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { - const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); - - return await provider.getPrometheusService(apiClient); - }); + const prometheusPromises = providers.map(provider => ( + this.cluster.getProxyKubeconfig() + .then(kubeConfig => kubeConfig.makeApiClient(CoreV1Api)) + .then(apiClient => provider.getPrometheusService(apiClient)) + )); const resolvedPrometheusServices = await Promise.all(prometheusPromises); - return resolvedPrometheusServices.filter(n => n)[0]; + return resolvedPrometheusServices.find(n => n); } async getPrometheusPath(): Promise { diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index 74afb166b1..f761e5ef75 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -1,11 +1,13 @@ import yaml from "js-yaml"; import { readFile } from "fs-extra"; -import { promiseExec } from "../promise-exec"; +import { promiseExec, stdOptimizedPromiseExec } from "../promise-exec"; import { helmCli } from "./helm-cli"; import { Singleton } from "../../common/utils/singleton"; -import { customRequestPromise } from "../../common/request"; +import { defaultGotOptions } from "../../common/got-opts"; import orderBy from "lodash/orderBy"; import logger from "../logger"; +import got, { OptionsOfJSONResponseBody } from "got"; +import { filter } from "lodash"; export type HelmEnv = Record & { HELM_REPOSITORY_CACHE?: string; @@ -36,14 +38,13 @@ export class HelmRepoManager extends Singleton { protected initialized: boolean; async loadAvailableRepos(): Promise { - const res = await customRequestPromise({ - uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json", - json: true, - resolveWithFullResponse: true, - timeout: 10000, - }); + const options = { + ...defaultGotOptions() as OptionsOfJSONResponseBody, + timeout: 10000 + }; + const r = await got("https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json", options); - return orderBy(res.body, repo => repo.name); + return orderBy(r.body, repo => repo.name); } async init() { @@ -60,7 +61,7 @@ export class HelmRepoManager extends Singleton { protected async parseHelmEnv() { const helm = await helmCli.binaryPath(); const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => { - throw(error.stderr); + throw (error.stderr); }); const lines = stdout.split(/\r?\n/); // split by new line feed const env: HelmEnv = {}; @@ -114,50 +115,41 @@ export class HelmRepoManager extends Singleton { public async update() { const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => { - return { stdout: error.stdout }; - }); + const command = `"${helm}" repo update`; - return stdout; + return stdOptimizedPromiseExec(command); } public async addRepo({ name, url }: HelmRepo) { logger.info(`[HELM]: adding repo "${name}" from ${url}`); const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => { - throw(error.stderr); - }); + const command = `"${helm}" repo add ${name} ${url}`; - return stdout; + return stdOptimizedPromiseExec(command); } public async addСustomRepo(repoAttributes : HelmRepo) { logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`); const helm = await helmCli.binaryPath(); + const args = filter([ + repoAttributes.insecureSkipTlsVerify && "--insecure-skip-tls-verify", + repoAttributes.username && `--username "${repoAttributes.username}"`, + repoAttributes.username && `--username "${repoAttributes.username}"`, + repoAttributes.caFile && `--ca-file "${repoAttributes.caFile}"`, + repoAttributes.keyFile && `--key-file "${repoAttributes.keyFile}"`, + repoAttributes.certFile && `--cert-file "${repoAttributes.certFile}"`, + ]); + const command = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url} ${args.join(" ")}`; - const insecureSkipTlsVerify = repoAttributes.insecureSkipTlsVerify ? " --insecure-skip-tls-verify" : ""; - const username = repoAttributes.username ? ` --username "${repoAttributes.username}"` : ""; - const password = repoAttributes.password ? ` --password "${repoAttributes.password}"` : ""; - const caFile = repoAttributes.caFile ? ` --ca-file "${repoAttributes.caFile}"` : ""; - const keyFile = repoAttributes.keyFile ? ` --key-file "${repoAttributes.keyFile}"` : ""; - const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : ""; - - const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${certFile}`; - const { stdout } = await promiseExec(addRepoCommand).catch((error) => { - throw(error.stderr); - }); - - return stdout; + return stdOptimizedPromiseExec(command); } public async removeRepo({ name, url }: HelmRepo): Promise { logger.info(`[HELM]: removing repo "${name}" from ${url}`); const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => { - throw(error.stderr); - }); + const command = `"${helm}" repo remove ${name}`; - return stdout; + return stdOptimizedPromiseExec(command); } } diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 7e0d6ed5c7..49427feca2 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -1,15 +1,21 @@ import { app, remote } from "electron"; -import path from "path"; import fs from "fs"; -import { promiseExec } from "./promise-exec"; -import logger from "./logger"; import { ensureDir, pathExists } from "fs-extra"; +import got from "got"; +import path from "path"; import * as lockFile from "proper-lockfile"; -import { helmCli } from "./helm/helm-cli"; +import stream from "stream"; +import { promisify } from "util"; + +import { defaultGotOptions, GotStreamFunctionOptions } from "../common/got-opts"; import { userStore } from "../common/user-store"; -import { customRequest } from "../common/request"; import { getBundledKubectlVersion } from "../common/utils/app-version"; -import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; +import { isDevelopment, isTestEnv, isWindows } from "../common/vars"; +import { helmCli } from "./helm/helm-cli"; +import logger from "./logger"; +import { promiseExec } from "./promise-exec"; + +const pipeline = promisify(stream.pipeline); const bundledVersion = getBundledKubectlVersion(); const kubectlMap: Map = new Map([ @@ -273,33 +279,22 @@ export class Kubectl { logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); - return new Promise((resolve, reject) => { - const stream = customRequest({ - url: this.url, - gzip: true, - }); - const file = fs.createWriteStream(this.path); + logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); + const options: GotStreamFunctionOptions = { + ...defaultGotOptions(), + decompress: true, + isStream: true, + }; - stream.on("complete", () => { - logger.debug("kubectl binary download finished"); - file.end(); - }); - stream.on("error", (error) => { - logger.error(error); - fs.unlink(this.path, () => { - // do nothing - }); - reject(error); - }); - file.on("close", () => { - logger.debug("kubectl binary download closed"); - fs.chmod(this.path, 0o755, (err) => { - if (err) reject(err); - }); - resolve(); - }); - stream.pipe(file); - }); + try { + // TODO: improve the UI for this to use some sort of loading bar TUI + await pipeline( + got.stream(this.url, options), + fs.createWriteStream(this.path) + ); + } catch (err) { + logger.error("Failed to download kubectl:", err); + } } protected async writeInitScripts() { diff --git a/src/main/lens-binary.ts b/src/main/lens-binary.ts index 3cf5a5fce7..73780f8ee2 100644 --- a/src/main/lens-binary.ts +++ b/src/main/lens-binary.ts @@ -1,17 +1,22 @@ import path from "path"; import fs from "fs"; -import request from "request"; -import { ensureDir, pathExists } from "fs-extra"; +import fse from "fs-extra"; import * as tar from "tar"; import { isWindows } from "../common/vars"; import winston from "winston"; +import { GotStreamFunctionOptions } from "../common/got-opts"; +import got from "got"; +import stream from "stream"; +import { promisify } from "util"; + +const pipeline = promisify(stream.pipeline); export type LensBinaryOpts = { version: string; baseDir: string; originalBinaryName: string; newBinaryName?: string; - requestOpts?: request.Options; + requestOpts?: GotStreamFunctionOptions; }; export class LensBinary { @@ -26,7 +31,7 @@ export class LensBinary { protected platformName: string; protected arch: string; protected originalBinaryName: string; - protected requestOpts: request.Options; + protected requestOpts: GotStreamFunctionOptions; protected logger: Console | winston.Logger; constructor(opts: LensBinaryOpts) { @@ -63,7 +68,7 @@ export class LensBinary { } } - public setLogger(logger: Console | winston.Logger) { + public setLogger(logger: Console | winston.Logger) { this.logger = logger; } @@ -110,88 +115,71 @@ export class LensBinary { } protected async checkBinary() { - const exists = await pathExists(this.getBinaryPath()); - - return exists; + return fse.pathExists(this.getBinaryPath()); } public async ensureBinary() { - const isValid = await this.checkBinary(); + try { + if (await this.checkBinary()) { + return; + } + + await this.downloadBinary(); + + if (this.tarPath) { + await this.untarBinary(); + } + + if (this.originalBinaryName !== this.binaryName) { + await this.renameBinary(); + } - if (!isValid) { - await this.downloadBinary().catch((error) => { - this.logger.error(error); - }); - if (this.tarPath) await this.untarBinary(); - if (this.originalBinaryName != this.binaryName) await this.renameBinary(); this.logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`); + } catch (err) { + this.logger.error(err); } } protected async untarBinary() { - return new Promise(resolve => { - this.logger.debug(`Extracting ${this.originalBinaryName} binary`); - tar.x({ + this.logger.debug(`Extracting ${this.originalBinaryName} binary`); + + try { + await tar.x({ file: this.tarPath, cwd: this.dirname - }).then((() => { - resolve(); - })); - }); + }); + } catch (err) { + this.logger.error(`failed to extract ${this.originalBinaryName}: `, err); + } } protected async renameBinary() { - return new Promise((resolve, reject) => { - this.logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`); - fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => { - if (err) { - reject(err); - } - else { - resolve(); - } - }); - }); + this.logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`); + + return fse.promises.rename(this.getOriginalBinaryPath(), this.getBinaryPath()); } protected async downloadBinary() { const binaryPath = this.tarPath || this.getBinaryPath(); - await ensureDir(this.getBinaryDir(), 0o755); + await fse.ensureDir(this.getBinaryDir(), 0o755); - const file = fs.createWriteStream(binaryPath); const url = this.getUrl(); - - this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`); - const requestOpts: request.UriOptions & request.CoreOptions = { - uri: url, - gzip: true, + const requestOpts: GotStreamFunctionOptions = { + decompress: true, + isStream: true, ...this.requestOpts }; - const stream = request(requestOpts); - stream.on("complete", () => { - this.logger.info(`Download of ${this.originalBinaryName} finished`); - file.end(); - }); + this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`); - stream.on("error", (error) => { - this.logger.error(error); - fs.unlink(binaryPath, () => { - // do nothing - }); - throw(error); - }); - - return new Promise((resolve, reject) => { - file.on("close", () => { - this.logger.debug(`${this.originalBinaryName} binary download closed`); - if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => { - if (err) reject(err); - }); - resolve(); - }); - stream.pipe(file); - }); + try { + await pipeline( + got.stream(url, requestOpts), + fs.createWriteStream(binaryPath), + ); + } catch (err) { + this.logger.error("Failed to download kubectl:", err); + } } } diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts index c649560c85..718724a2ae 100644 --- a/src/main/prometheus/provider-registry.ts +++ b/src/main/prometheus/provider-registry.ts @@ -1,6 +1,6 @@ import { CoreV1Api } from "@kubernetes/client-node"; -export type PrometheusClusterQuery = { +export interface PrometheusClusterQuery { memoryUsage: string; memoryRequests: string; memoryLimits: string; @@ -11,18 +11,18 @@ export type PrometheusClusterQuery = { cpuCapacity: string; podUsage: string; podCapacity: string; -}; +} -export type PrometheusNodeQuery = { +export interface PrometheusNodeQuery { memoryUsage: string; memoryCapacity: string; cpuUsage: string; cpuCapacity: string; fsSize: string; fsUsage: string; -}; +} -export type PrometheusPodQuery = { +export interface PrometheusPodQuery { memoryUsage: string; memoryRequests: string; memoryLimits: string; @@ -32,25 +32,26 @@ export type PrometheusPodQuery = { fsUsage: string; networkReceive: string; networkTransmit: string; -}; +} -export type PrometheusPvcQuery = { +export interface PrometheusPvcQuery { diskUsage: string; diskCapacity: string; -}; +} -export type PrometheusIngressQuery = { +export interface PrometheusIngressQuery { bytesSentSuccess: string; bytesSentFailure: string; requestDurationSeconds: string; responseDurationSeconds: string; -}; +} export type PrometheusQueryOpts = { [key: string]: string | any; }; export type PrometheusQuery = PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery; +export type PrometheusQueryKey = keyof PrometheusNodeQuery | keyof PrometheusClusterQuery | keyof PrometheusPodQuery | keyof PrometheusPvcQuery | keyof PrometheusIngressQuery; export type PrometheusService = { id: string; diff --git a/src/main/promise-exec.ts b/src/main/promise-exec.ts index 426bca4c23..2e30aa9c31 100644 --- a/src/main/promise-exec.ts +++ b/src/main/promise-exec.ts @@ -2,3 +2,13 @@ import * as util from "util"; import { exec } from "child_process"; export const promiseExec = util.promisify(exec); + +export async function stdOptimizedPromiseExec(url: string): Promise { + try { + const { stdout } = await promiseExec(url); + + return stdout; + } catch ({ stderr }) { + throw stderr; + } +} diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts index c3c0bdb353..c7051c76d7 100644 --- a/src/main/routes/helm-route.ts +++ b/src/main/routes/helm-route.ts @@ -52,7 +52,7 @@ class HelmApiRoute extends LensApi { const { cluster, params, payload, response } = request; try { - const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload ); + const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload); this.respondJson(response, result); } catch (error) { @@ -81,7 +81,7 @@ class HelmApiRoute extends LensApi { const result = await helmService.listReleases(cluster, params.namespace); this.respondJson(response, result); - } catch(error) { + } catch (error) { logger.debug(error); this.respondText(response, error, 422); } diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index d132f7b0ae..b5d90d183e 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -4,10 +4,15 @@ import { LensApi } from "../lens-api"; import { Cluster, ClusterMetadataKey } from "../cluster"; import { ClusterPrometheusMetadata } from "../../common/cluster-store"; import logger from "../logger"; +import { PrometheusQueryKey } from "../prometheus/provider-registry"; -export type IMetricsQuery = string | string[] | { +export interface MetricsQuery { [metricName: string]: string; -}; +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} // This is used for backoff retry tracking. const MAX_ATTEMPTS = 5; @@ -29,7 +34,7 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa throw new Error("Metrics not available"); } - await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request + await delay((attempt + 1) * 1000); // add delay before repeating request } } } @@ -40,9 +45,14 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa return Promise.all(queries.map(loadMetric)); } +export interface MetricsResult { + status?: string; + data?: any; +} + class MetricsRoute extends LensApi { async routeMetrics({ response, cluster, payload, query }: LensApiRequest) { - const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); + const queryParams: MetricsQuery = Object.fromEntries(query.entries()); const prometheusMetadata: ClusterPrometheusMetadata = {}; try { @@ -72,7 +82,7 @@ class MetricsRoute extends LensApi { this.respondJson(response, data); } else { const queries = Object.entries(payload).map(([queryName, queryOpts]) => ( - (prometheusProvider.getQueries(queryOpts) as Record)[queryName] + (prometheusProvider.getQueries(queryOpts) as Record)[queryName as PrometheusQueryKey] )); const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); diff --git a/src/renderer/api/endpoints/metrics.api.ts b/src/renderer/api/endpoints/metrics.api.ts index 9c3ee74adc..45515a5647 100644 --- a/src/renderer/api/endpoints/metrics.api.ts +++ b/src/renderer/api/endpoints/metrics.api.ts @@ -2,7 +2,7 @@ import moment from "moment"; import { apiBase } from "../index"; -import type { IMetricsQuery } from "../../../main/routes/metrics-route"; +import type { MetricsQuery } from "../../../main/routes/metrics-route"; export interface IMetrics { status: string; @@ -34,7 +34,7 @@ export interface IMetricsReqParams { } export const metricsApi = { - async getMetrics(query: T, reqParams: IMetricsReqParams = {}): Promise { + async getMetrics(query: T, reqParams: IMetricsReqParams = {}): Promise { const { range = 3600, step = 60, namespace } = reqParams; let { start, end } = reqParams; diff --git a/webpack.main.ts b/webpack.main.ts index 6972c92457..35ab29787b 100755 --- a/webpack.main.ts +++ b/webpack.main.ts @@ -13,7 +13,8 @@ export default function (): webpack.Configuration { context: __dirname, target: "electron-main", mode: isProduction ? "production" : "development", - devtool: isProduction ? "source-map" : "cheap-eval-source-map", + // devtool: isProduction ? "source-map" : "cheap-eval-source-map", + devtool: "source-map", cache: isDevelopment, entry: { main: path.resolve(mainDir, "index.ts"), diff --git a/yarn.lock b/yarn.lock index 2a39325781..50b8c9eab0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -972,6 +972,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@sindresorhus/is@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4" + integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ== + "@sinonjs/commons@^1.7.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" @@ -993,10 +998,17 @@ dependencies: defer-to-connect "^1.0.1" +"@szmarczak/http-timer@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + "@testing-library/dom@>=7": - version "7.29.4" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.4.tgz#1647c2b478789621ead7a50614ad81ab5ae5b86c" - integrity sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA== + version "7.30.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.30.0.tgz#53697851f7708a1448cc30b74a2ea056dd709cd6" + integrity sha512-v4GzWtltaiDE0yRikLlcLAfEiiK8+ptu6OuuIebm9GdC2XlZTNDPGEfM2UkEtnH7hr9TRq2sivT5EA9P1Oy7bw== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" @@ -1106,6 +1118,16 @@ dependencies: "@types/node" "*" +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -1308,6 +1330,11 @@ "@types/tapable" "*" "@types/webpack" "*" +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + "@types/http-proxy-middleware@*": version "0.19.3" resolved "https://registry.yarnpkg.com/@types/http-proxy-middleware/-/http-proxy-middleware-0.19.3.tgz#b2eb96fbc0f9ac7250b5d9c4c53aade049497d03" @@ -1411,6 +1438,13 @@ resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.0.tgz#13c62db22a34d9c411364fac79fd374d63445aa1" integrity sha512-v7qlPA0VpKUlEdhghbDqRoKMxFB3h3Ch688TApBJ6v+XLDdvWCGLJIYiPKGZnS6MAOie+IorCfNYVHOPIHSWwQ== +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + "@types/lodash@^4.14.155": version "4.14.155" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" @@ -1665,6 +1699,13 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/retry@*": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -3262,6 +3303,11 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + cacheable-request@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" @@ -3288,6 +3334,19 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + call-bind@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce" @@ -4436,6 +4495,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -6328,6 +6392,23 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" +got@^11.8.1: + version "11.8.1" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.1.tgz#df04adfaf2e782babb3daabc79139feec2f7e85d" + integrity sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + got@^6.7.1: version "6.7.1" resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -6606,6 +6687,11 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" +hpagent@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.1.tgz#66f67f16e5c7a8b59a068e40c2658c2c749ad5e2" + integrity sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ== + html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -6751,6 +6837,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.0-beta.5.2" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" + integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -8114,6 +8208,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -8315,6 +8414,13 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" + integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== + dependencies: + json-buffer "3.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -11177,6 +11283,11 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + qw@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" @@ -11761,6 +11872,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resolve-alpn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c" + integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -11830,6 +11946,13 @@ responselike@1.0.2, responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"