diff --git a/package.json b/package.json index b4428bed57..3de34fef00 100644 --- a/package.json +++ b/package.json @@ -234,8 +234,6 @@ "react-dom": "^17.0.1", "react-router": "^5.2.0", "readable-stream": "^3.6.0", - "request": "^2.88.2", - "request-promise-native": "^1.0.8", "semver": "^7.3.2", "serializr": "^2.0.3", "shell-env": "^3.0.1", @@ -291,8 +289,6 @@ "@types/react-select": "^3.0.13", "@types/react-window": "^1.8.2", "@types/readable-stream": "^2.3.9", - "@types/request": "^2.48.5", - "@types/request-promise-native": "^1.0.17", "@types/semver": "^7.2.0", "@types/sharp": "^0.26.0", "@types/shelljs": "^0.8.8", diff --git a/src/common/utils/app-version.ts b/src/common/utils/app-version.ts index d625f44cd7..97ab58300c 100644 --- a/src/common/utils/app-version.ts +++ b/src/common/utils/app-version.ts @@ -1,4 +1,4 @@ -import requestPromise from "request-promise-native"; +import got from "got/dist/source"; import packageInfo from "../../../package.json"; export function getAppVersion(): string { @@ -14,11 +14,7 @@ export function getBundledExtensions(): string[] { } export async function getAppVersionFromProxyServer(proxyPort: number): Promise { - const response = await requestPromise({ - method: "GET", - uri: `http://localhost:${proxyPort}/version`, - resolveWithFullResponse: true - }); + const responseJson = await got(`http://localhost:${proxyPort}/version`).json(); - return JSON.parse(response.body).version; + return responseJson.version; } diff --git a/src/common/utils/downloadFile.ts b/src/common/utils/downloadFile.ts index dfa549da07..fcd75dfbaa 100644 --- a/src/common/utils/downloadFile.ts +++ b/src/common/utils/downloadFile.ts @@ -1,4 +1,4 @@ -import request from "request"; +import got, { CancelableRequest } from "got"; export interface DownloadFileOptions { url: string; @@ -6,32 +6,6 @@ export interface DownloadFileOptions { timeout?: number; } -export interface DownloadFileTicket { - url: string; - promise: Promise; - cancel(): void; -} - -export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket { - const fileChunks: Buffer[] = []; - const req = request(url, { gzip, timeout }); - const promise: Promise = new Promise((resolve, reject) => { - req.on("data", (chunk: Buffer) => { - fileChunks.push(chunk); - }); - req.once("error", err => { - reject({ url, err }); - }); - req.once("complete", () => { - resolve(Buffer.concat(fileChunks)); - }); - }); - - return { - url, - promise, - cancel() { - req.abort(); - } - }; +export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): CancelableRequest { + return got(url, { timeout, decompress: gzip }).buffer(); } diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts index 61d3526c85..b79f96eb04 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster-detectors/base-cluster-detector.ts @@ -1,5 +1,5 @@ -import request, { RequestPromiseOptions } from "request-promise-native"; -import { Cluster } from "../cluster"; +import { OptionsOfJSONResponseBody, Response } from "got"; +import { Cluster, k8sRequest } from "../cluster"; export type ClusterDetectionResult = { value: string | number | boolean @@ -18,17 +18,10 @@ export class BaseClusterDetector { return null; } - protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { - const apiUrl = this.cluster.kubeProxyUrl + path; + protected async k8sRequest(path: string, options: OptionsOfJSONResponseBody = {}): Promise<[Response, T]> { + options.headers ??= {}; + options.headers["Host"] ??= `${this.cluster.id}.${this.cluster.kubeProxyUrl.host}`; // required in ClusterManager.getClusterForRequest()'' - return request(apiUrl, { - json: true, - timeout: 30000, - ...options, - headers: { - Host: `${this.cluster.id}.${this.cluster.kubeProxyUrl.host}`, // required in ClusterManager.getClusterForRequest() - ...(options.headers || {}), - }, - }); + return k8sRequest(this.cluster.getUrlTo(path), options); } } diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts index 810955afae..cac69f87ae 100644 --- a/src/main/cluster-detectors/cluster-id-detector.ts +++ b/src/main/cluster-detectors/cluster-id-detector.ts @@ -19,8 +19,8 @@ export class ClusterIdDetector extends BaseClusterDetector { } protected async getDefaultNamespaceId() { - const response = await this.k8sRequest("/api/v1/namespaces/default"); + const [, responseJson] = await this.k8sRequest("/api/v1/namespaces/default"); - return response.metadata.uid; + return responseJson.metadata.uid; } } diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index 73d5541d17..e1541f862d 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -39,7 +39,7 @@ export class DistributionDetector extends BaseClusterDetector { if (this.isK0s()) { return { value: "k0s", accuracy: 80}; } - + if (this.isVMWare()) { return { value: "vmware", accuracy: 90}; } @@ -90,9 +90,9 @@ export class DistributionDetector extends BaseClusterDetector { public async getKubernetesVersion() { if (this.cluster.version) return this.cluster.version; - const response = await this.k8sRequest("/version"); + const [,responseJson] = await this.k8sRequest("/version"); - return response.gitVersion; + return responseJson.gitVersion; } protected isGKE() { @@ -158,7 +158,7 @@ export class DistributionDetector extends BaseClusterDetector { protected isK0s() { return this.version.includes("-k0s"); } - + protected isAlibaba() { return this.version.includes("-aliyun"); } @@ -169,9 +169,9 @@ export class DistributionDetector extends BaseClusterDetector { protected async isOpenshift() { try { - const response = await this.k8sRequest(""); + const [, responseJson] = await this.k8sRequest(""); - return response.paths?.includes("/apis/project.openshift.io"); + return responseJson.paths?.includes("/apis/project.openshift.io"); } catch (e) { return false; } diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts index 0a9bcf9f74..4251ee698d 100644 --- a/src/main/cluster-detectors/last-seen-detector.ts +++ b/src/main/cluster-detectors/last-seen-detector.ts @@ -7,7 +7,7 @@ export class LastSeenDetector extends BaseClusterDetector { public async detect() { if (!this.cluster.accessible) return null; - await this.k8sRequest("/version"); + await this.k8sRequest("/version"); return { value: new Date().toJSON(), accuracy: 100 }; } diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster-detectors/nodes-count-detector.ts index 45584df5bd..07794b31c2 100644 --- a/src/main/cluster-detectors/nodes-count-detector.ts +++ b/src/main/cluster-detectors/nodes-count-detector.ts @@ -12,8 +12,8 @@ export class NodesCountDetector extends BaseClusterDetector { } protected async getNodeCount(): Promise { - const response = await this.k8sRequest("/api/v1/nodes"); + const [, responseJson] = await this.k8sRequest("/api/v1/nodes"); - return response.items.length; + return responseJson.items.length; } } diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster-detectors/version-detector.ts index b19979db8a..8c56283b30 100644 --- a/src/main/cluster-detectors/version-detector.ts +++ b/src/main/cluster-detectors/version-detector.ts @@ -12,8 +12,8 @@ export class VersionDetector extends BaseClusterDetector { } public async getKubernetesVersion() { - const response = await this.k8sRequest("/version"); + const [, responseJson] = await this.k8sRequest("/version", { retry: 0 }); - return response.gitVersion; + return responseJson.gitVersion; } } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index d57919fbba..8bdaf7a630 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -14,7 +14,7 @@ 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 got, { OptionsOfJSONResponseBody, Response } from "got"; import { URL } from "url"; export enum ClusterStatus { @@ -51,6 +51,24 @@ export interface ClusterState { isGlobalWatchEnabled: boolean; } +export async function k8sRequest(url: string | URL, options: OptionsOfJSONResponseBody = {}): Promise<[Response, T]> { + options.timeout ??= 5000; + options.headers ??= {}; + options.headers["content-type"] ??= "application/json"; + options.throwHttpErrors ??= true; + + try { + const resp = got(url, options); + const jsonResp = resp.json(); + + return await Promise.all([resp, jsonResp]); + } catch(error) { + logger.error(`[REQUEST]: failed to get ${url}`, { error }); + + throw error; + } +} + /** * Cluster * @@ -495,7 +513,7 @@ export class Cluster implements ClusterModel, ClusterState { return this.kubeconfigManager.getPath(); } - private getUrlTo(path: string): URL { + public getUrlTo(path: string): URL { const url = new URL(this.kubeProxyUrl.toString()); url.pathname += path; @@ -538,34 +556,36 @@ export class Cluster implements ClusterModel, ClusterState { const versionData = await versionDetector.detect(); this.metadata.version = versionData.value; - this.failureReason = null; return ClusterStatus.AccessGranted; } catch (error) { - logger.error(`Failed to connect cluster "${this.contextName}": ${error}`); + logger.error(`Failed to connect cluster "${this.contextName}"`, { error }); + + if (error instanceof got.TimeoutError) { + this.failureReason = "Connection timed out"; + + return ClusterStatus.Offline; + } if (error.statusCode) { if (error.statusCode >= 400 && error.statusCode < 500) { this.failureReason = "Invalid credentials"; return ClusterStatus.AccessDenied; - } else { - this.failureReason = error.error || error.message; - - return ClusterStatus.Offline; } - } else if (error.failed === true) { - if (error.timedOut === true) { - this.failureReason = "Connection timed out"; - return ClusterStatus.Offline; - } else { - this.failureReason = "Failed to fetch credentials"; + this.failureReason = error.error || error.message; - return ClusterStatus.AccessDenied; - } + return ClusterStatus.Offline; } + + if (error.failed === true) { + this.failureReason = "Failed to fetch credentials"; + + return ClusterStatus.AccessDenied; + } + this.failureReason = error.message; return ClusterStatus.Offline; diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index cc660b0f48..66d3f9f7d8 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -12,15 +12,17 @@ export interface MetricsQuery { // prometheus metrics loader async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record): Promise { const queries = promQueries.map(p => p.trim()); - const loaders = new Map>(); + const loaders = new Map>(); - async function loadMetric(query: string): Promise { + async function loadMetric(query: string) { const searchParams = { query, ...queryParams }; return loaders.get(query) ?? loaders.set(query, cluster.getMetrics(prometheusPath, { searchParams, retry: 5 })).get(query); } - return Promise.all(queries.map(loadMetric)); + const responses = await Promise.all(queries.map(loadMetric)); + + return responses.map(([, respJson]) => respJson); } export interface MetricsResult { @@ -69,7 +71,7 @@ class MetricsRoute extends LensApi { } prometheusMetadata.success = true; } catch (error) { - logger.error("[METRICS]: failed to load metrics", { error }); + logger.error(`[METRICS]: failed to load metrics: ${error}`, { error }); prometheusMetadata.success = false; this.respondJson(response, {}); diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index c14a6988b6..8594b64cde 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -170,8 +170,7 @@ export class Extensions extends React.Component { // install via url // fixme: improve error messages for non-tar-file URLs if (InputValidators.isUrl.validate(installPath)) { - const { promise: filePromise } = downloadFile({ url: installPath, timeout: 60000 /*1m*/ }); - const data = await filePromise; + const data = await downloadFile({ url: installPath, timeout: 60000 /*1m*/ }); await this.requestInstall({ fileName, data }); } diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 03711e41fc..9b0cdb62c6 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -44,7 +44,7 @@ export class ClustersMenu extends React.Component { click: actions.showSettings })); - if (cluster.online) { + if (cluster.online || cluster.failureReason) { menu.append(new MenuItem({ label: `Disconnect`, click: actions.disconnect diff --git a/yarn.lock b/yarn.lock index 50b8c9eab0..d77f050d91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1682,14 +1682,7 @@ resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6" integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y= -"@types/request-promise-native@^1.0.17": - version "1.0.17" - resolved "https://registry.yarnpkg.com/@types/request-promise-native/-/request-promise-native-1.0.17.tgz#74a2d7269aebf18b9bdf35f01459cf0a7bfc7fab" - integrity sha512-05/d0WbmuwjtGMYEdHIBZ0tqMJJQ2AD9LG2F6rKNBGX1SSFR27XveajH//2N/XYtual8T9Axwl+4v7oBtPUZqg== - dependencies: - "@types/request" "*" - -"@types/request@*", "@types/request@^2.47.1", "@types/request@^2.48.5": +"@types/request@^2.47.1": version "2.48.5" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==