From 5eca8bc01cdb70f1bffc43669be0506404986d70 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 11 Jul 2020 13:17:18 +0300 Subject: [PATCH] clean up / fixes Signed-off-by: Roman --- src/common/cluster-store.ts | 4 +- src/common/ipc-helpers.ts | 2 +- src/common/vars.ts | 14 +-- src/main/cluster-manager.ts | 4 +- src/main/cluster.ts | 9 +- src/main/context-handler.ts | 35 +++--- src/main/kube-auth-proxy.ts | 4 +- src/main/kubeconfig-manager.ts | 12 +- src/main/lens-proxy.ts | 48 ++++---- src/main/port.ts | 2 +- src/main/router.ts | 104 +++++++++--------- src/main/routes/metrics-route.ts | 54 ++++----- src/renderer/api/endpoints/helm-charts.api.ts | 8 +- .../api/endpoints/helm-releases.api.ts | 18 +-- .../api/endpoints/resource-applier.api.ts | 4 +- src/renderer/api/index.ts | 18 +-- src/renderer/api/kube-api.ts | 9 +- src/renderer/api/kube-json-api.ts | 15 --- src/renderer/api/kube-watch-api.ts | 3 +- 19 files changed, 168 insertions(+), 199 deletions(-) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 9d1cf09f7a..93b73d5702 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -18,7 +18,7 @@ export interface ClusterModel { kubeConfigPath: string; /** @deprecated */ - kubeConfig?: string; // kube-config yaml + kubeConfig?: string; // yaml } export interface ClusterPreferences { @@ -48,7 +48,7 @@ export class ClusterStore extends BaseStore { }); } - @observable activeCluster: ClusterId; // todo: use active "context" from kube-config? + @observable activeCluster: ClusterId; // todo: current-context from kube-config? @observable removedClusters = observable.map(); @observable clusters = observable.map(); diff --git a/src/common/ipc-helpers.ts b/src/common/ipc-helpers.ts index 259f20c3b5..63c8b8301e 100644 --- a/src/common/ipc-helpers.ts +++ b/src/common/ipc-helpers.ts @@ -15,7 +15,7 @@ export interface IpcMessageHandler { (...args: any[]): any; } -export function sendMessageToRenderer(channel: IpcChannel, ...args: any[]) { +export function sendMessage(channel: IpcChannel, ...args: any[]) { webContents.getFocusedWebContents().send(channel, ...args); } diff --git a/src/common/vars.ts b/src/common/vars.ts index 065d3d969b..bbc5c17072 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -10,10 +10,10 @@ export const isDevelopment = isDebugging || !isProduction; export const isTestEnv = !!process.env.JEST_WORKER_ID; export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}` -export const appProto = "lens" // app's "userData" folder (e.g. "lens://icons/logo.svg") -export const staticProto = "static" // static content folder (e.g. "static://RELEASE_NOTES.md") +export const appProto = "lens" // app.getPath("userData") folder +export const staticProto = "static" // static folder (e.g. "static://RELEASE_NOTES.md") -// Paths +// System paths export const contextDir = process.cwd(); export const staticDir = path.join(contextDir, "static"); export const outDir = path.join(contextDir, "out"); @@ -23,12 +23,8 @@ export const htmlTemplate = path.resolve(rendererDir, "template.html"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); // Apis -export const apiPrefix = { - BASE: '/api', - KUBE_BASE: '/api-kube', // kubernetes cluster api - KUBE_HELM: '/api-helm', // helm charts api - KUBE_RESOURCE_APPLIER: "/api-resource", -}; +export const apiPrefix = "/api-local" // local router apis +export const apiKubePrefix = "/api-kube" // k8s cluster apis // Links export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 1dc161d2b5..a49eccecc8 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -4,7 +4,7 @@ import path from "path" import http from "http" import { copyFile, ensureDir } from "fs-extra" import filenamify from "filenamify" -import { apiPrefix, appProto } from "../common/vars"; +import { apiKubePrefix, appProto } from "../common/vars"; import { ClusterId, ClusterModel, clusterStore } from "../common/cluster-store" import { handleMessages } from "../common/ipc-helpers"; import { ClusterIpcMessage } from "../common/ipc-messages"; @@ -100,7 +100,7 @@ export class ClusterManager { cluster = this.getCluster(clusterId) if (cluster) { // we need to swap path prefix so that request is proxied to kube api - req.url = req.url.replace(`/${clusterId}`, apiPrefix.KUBE_BASE) + req.url = req.url.replace(`/${clusterId}`, apiKubePrefix) } } } else { diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 9d299cfa9e..1c0c038eb3 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -37,8 +37,8 @@ export class Cluster implements ClusterModel { protected kubeconfigManager: KubeconfigManager; @observable initialized = false; - @observable workspace: string; @observable contextName: string; + @observable workspace: string; @observable kubeConfigPath: string; @observable port: number; @observable url: string; // cluster-api url @@ -70,14 +70,15 @@ export class Cluster implements ClusterModel { @action async init() { try { - // fixme: all broken + // fixme this.contextHandler = new ContextHandler(this); - this.port = await this.contextHandler.resolveProxyPort(); // resolve port before KubeconfigManager - this.webContentUrl = `http://${this.id}.localhost:${this.port}`; + this.port = await this.contextHandler.ensurePort(); // resolve port before KubeconfigManager this.kubeAuthProxyUrl = `http://127.0.0.1:${this.port}`; this.kubeconfigManager = new KubeconfigManager(this); + // this.url = this.kubeconfigManager.getCurrentClusterServer(); // this.apiUrl = url.parse(this.url); + this.webContentUrl = `http://${this.id}.localhost:${this.port}`; logger.info(`[CLUSTER]: init success`, { id: this.id, diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index ac2bc8b6fe..9f3a5f629e 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -1,18 +1,19 @@ import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry" import type { ClusterPreferences } from "../common/cluster-store"; -import type { ServerOptions } from "http-proxy" import type { Cluster } from "./cluster" +import type httpProxy from "http-proxy" import { CoreV1Api } from "@kubernetes/client-node" +import { observable } from "mobx"; import { prometheusProviders } from "../common/prometheus-providers" import logger from "./logger" import { getFreePort } from "./port" import { KubeAuthProxy } from "./kube-auth-proxy" export class ContextHandler { - public proxyPort: number + @observable proxyPort: number; protected proxyServer: KubeAuthProxy - protected apiTarget: ServerOptions + protected apiTarget: httpProxy.ServerOptions protected prometheusProvider: string protected prometheusPath: string @@ -44,7 +45,7 @@ export class ContextHandler { } public async getPrometheusService(): Promise { - const providers = this.prometheusProvider ? prometheusProviders.filter((p, _) => p.id == this.prometheusProvider) : prometheusProviders + const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { const apiClient = this.cluster.proxyKubeconfig().makeApiClient(CoreV1Api) return await provider.getPrometheusService(apiClient) @@ -66,7 +67,7 @@ export class ContextHandler { return this.prometheusPath; } - public async getApiTarget(isWatchRequest = false): Promise { + public async getApiTarget(isWatchRequest = false): Promise { if (this.apiTarget && !isWatchRequest) { return this.apiTarget } @@ -78,24 +79,24 @@ export class ContextHandler { return apiTarget } - // fixme - protected async newApiTarget(timeout: number): Promise { + public async getApiTargetUrl(): Promise { + const port = await this.ensurePort(); + const { path } = this.cluster.apiUrl; + return `http://127.0.0.1:${port}${path}`; + } + + protected async newApiTarget(timeout: number): Promise { return { changeOrigin: true, + target: await this.getApiTargetUrl(), timeout: timeout, headers: { - "Host": this.cluster.apiUrl.hostname - }, - target: { - port: await this.resolveProxyPort(), - protocol: "http://", - host: "localhost", - path: this.cluster.apiUrl.path, - }, + "Host": this.cluster.apiUrl.hostname, + } } } - async resolveProxyPort(): Promise { + async ensurePort(): Promise { if (!this.proxyPort) { this.proxyPort = await getFreePort(); } @@ -104,7 +105,7 @@ export class ContextHandler { public async ensureServer() { if (!this.proxyServer) { - await this.resolveProxyPort(); + await this.ensurePort(); const proxyEnv = Object.assign({}, process.env) if (this.cluster.preferences.httpsProxy) { proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 1be8875040..a12113bbaa 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -1,6 +1,6 @@ import { ChildProcess, spawn } from "child_process" import { waitUntilUsed } from "tcp-port-used"; -import { sendMessageToRenderer } from "../common/ipc-helpers"; +import { sendMessage } from "../common/ipc-helpers"; import type { Cluster } from "./cluster" import { bundledKubectl, Kubectl } from "./kubectl" import logger from "./logger" @@ -84,7 +84,7 @@ export class KubeAuthProxy { const channel = `kube-auth:${this.cluster.id}` const message = { data, stream }; logger.debug(channel, message); - sendMessageToRenderer(channel, message); + sendMessage(channel, message); } public exit() { diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index 6a3f8fcf60..90f71f67c8 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -10,7 +10,11 @@ export class KubeconfigManager { protected tempFile: string constructor(protected cluster: Cluster) { - this.tempFile = this.createTemporaryKubeconfig(); + this.init(); + } + + protected async init() { + this.tempFile = await this.createTemporaryKubeconfig(); } getPath() { @@ -21,7 +25,7 @@ export class KubeconfigManager { * Creates new "temporary" kubeconfig that point to the kubectl-proxy. * This way any user of the config does not need to know anything about the auth etc. details. */ - protected createTemporaryKubeconfig(): string { + protected async createTemporaryKubeconfig(): Promise { fs.ensureDir(this.configDir); const path = `${this.configDir}/${randomFileName("kubeconfig")}`; const { contextName, contextHandler, kubeConfigPath } = this.cluster; @@ -29,7 +33,7 @@ export class KubeconfigManager { kubeConfig.clusters = [ { name: contextName, - server: `http://127.0.0.1:${contextHandler.proxyPort}`, // fixme: extract + server: await contextHandler.getApiTargetUrl(), skipTLSVerify: true, } ]; @@ -42,7 +46,7 @@ export class KubeconfigManager { user: "proxy", name: contextName, cluster: contextName, - // namespace: kubeConfig.getContextObject(contextName).namespace, + namespace: kubeConfig.getContextObject(contextName).namespace, } ]; logger.info(`Creating temp config for context "${contextName}" at "${path}"`); diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 86d2d745f4..3ed718bb5a 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -1,13 +1,13 @@ +import net from "net"; import http from "http"; import httpProxy from "http-proxy"; -import { Socket } from "net"; -import * as url from "url"; +import url from "url"; import * as WebSocket from "ws" -import { ContextHandler } from "./context-handler"; import * as nodeShell from "./node-shell-session" -import { ClusterManager } from "./cluster-manager" import { Router } from "./router" -import { apiPrefix } from "../common/vars"; +import { ClusterManager } from "./cluster-manager" +import { ContextHandler } from "./context-handler"; +import { apiKubePrefix } from "../common/vars"; import logger from "./logger" export class LensProxy { @@ -27,7 +27,7 @@ export class LensProxy { } listen(): this { - const proxyServer = this.buildProxyServer(); + const proxyServer = this.buildCustomProxy(); const { proxyPort } = this.clusterManager; this.proxyServer = proxyServer.listen(proxyPort); logger.info(`LensProxy server has started http://localhost:${proxyPort}`); @@ -40,21 +40,21 @@ export class LensProxy { this.closed = true } - protected buildProxyServer() { + protected buildCustomProxy(): http.Server { const proxy = this.createProxy(); - const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { + const customProxy = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { this.handleRequest(proxy, req, res); }); - proxyServer.on("upgrade", (req: http.IncomingMessage, socket: Socket, head: Buffer) => { + customProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { this.handleWsUpgrade(req, socket, head) }); - proxyServer.on("error", (err) => { + customProxy.on("error", (err) => { logger.error("proxy error", err) }); - return proxyServer; + return customProxy; } - protected createProxy() { + protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); proxy.on("proxyRes", (proxyRes, req, res) => { @@ -71,16 +71,18 @@ export class LensProxy { if (req.method !== "GET") { return } - const key = `${req.headers.host}${req.url}` - if (this.retryCounters.has(key)) { - logger.debug("Resetting proxy retry cache for url: " + key) - this.retryCounters.delete(key) + const reqUrl = `${req.headers.host}${req.url}` + if (this.retryCounters.has(reqUrl)) { + logger.debug("Resetting proxy retry cache for url: " + reqUrl) + this.retryCounters.delete(reqUrl) } }) proxy.on("error", (error, req, res, target) => { - if (this.closed) return; + if (this.closed) { + return; + } if (target) { - logger.debug("Failed proxy to target: " + JSON.stringify(target)) + logger.debug("Failed proxy to target: " + JSON.stringify(target, null, 2)); if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { const retryCounterKey = `${req.headers.host}${req.url}` const retryCount = this.retryCounters.get(retryCounterKey) || 0 @@ -112,11 +114,11 @@ export class LensProxy { })); } + // fixme: remove api prefix? protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise { - const prefix = apiPrefix.KUBE_BASE; - if (req.url.startsWith(prefix)) { + if (req.url.startsWith(apiKubePrefix)) { delete req.headers.authorization - req.url = req.url.replace(prefix, "") + req.url = req.url.replace(apiKubePrefix, "") const isWatchRequest = req.url.includes("watch=") return await contextHandler.getApiTarget(isWatchRequest) } @@ -137,11 +139,11 @@ export class LensProxy { if (proxyTarget) { proxy.web(req, res, proxyTarget) } else { - this.router.route(cluster, req, res); // todo: handle "not-found" if isBoom==true? + this.router.route(cluster, req, res); } } - protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) { + protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) { const wsServer = this.createWsListener(); wsServer.handleUpgrade(req, socket, head, (con) => { wsServer.emit("connection", con, req); diff --git a/src/main/port.ts b/src/main/port.ts index 0669187f65..90b636cf78 100644 --- a/src/main/port.ts +++ b/src/main/port.ts @@ -1,7 +1,7 @@ import logger from "./logger" import { createServer, AddressInfo } from "net" -// todo: replace with https://github.com/http-party/node-portfinder ? +// todo: use https://github.com/http-party/node-portfinder ? const getNextAvailablePort = () => { logger.debug("getNextAvailablePort() start") diff --git a/src/main/router.ts b/src/main/router.ts index bed47fce2c..2bab4253fc 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -9,37 +9,28 @@ import { resourceApplierApi } from "./resource-applier-api" import { apiPrefix, appName, outDir } from "../common/vars"; import { configRoute, kubeconfigRoute, metricsRoute, portForwardRoute, watchRoute } from "./routes"; -const mimeTypes: Record = { - "html": "text/html", - "txt": "text/plain", - "css": "text/css", - "gif": "image/gif", - "jpg": "image/jpeg", - "png": "image/png", - "svg": "image/svg+xml", - "js": "application/javascript", - "woff2": "font/woff2", - "ttf": "font/ttf" -}; - -interface RouteParams { - [key: string]: string | undefined; +export interface RouterRequestOpts

= any> { + req: http.IncomingMessage; + res: http.ServerResponse; + cluster: Cluster; + url: URL; + params: P; // https://hapi.dev/module/call/api } -export type LensApiRequest = { +export interface LensApiRequest { + path: string; + payload: D; + params: P; cluster: Cluster; - payload: any; - raw: { - req: http.IncomingMessage; - }; - params: RouteParams; response: http.ServerResponse; query: URLSearchParams; - path: string; + raw: { + req: http.IncomingMessage; + } } export class Router { - protected router: any + protected router: any; public constructor() { this.router = new Call.Router(); @@ -47,20 +38,23 @@ export class Router { } public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { - const reqUrl = new URL(req.url, "http://localhost"); - const path = reqUrl.pathname + const url = new URL(req.url, "http://localhost"); + const path = url.pathname const method = req.method.toLowerCase() const matchingRoute = this.router.route(method, path); - const routeExists = !matchingRoute.isBoom; - if (routeExists) { - const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params }) + const routeFound = !matchingRoute.isBoom; + if (routeFound) { + const request = await this.getRequest({ + req, res, cluster, url, + params: matchingRoute.params + }); await matchingRoute.route(request) return true } return false; } - protected async getRequest(opts: { req: http.IncomingMessage; res: http.ServerResponse; cluster: Cluster; url: URL; params: RouteParams }) { + protected async getRequest(opts: RouterRequestOpts) { const { req, res, url, cluster, params } = opts const { payload } = await Subtext.parse(req, null, { parse: true, output: 'data' }); const request: LensApiRequest = { @@ -78,6 +72,18 @@ export class Router { } protected getMimeType(filename: string) { + const mimeTypes: Record = { + html: "text/html", + txt: "text/plain", + css: "text/css", + gif: "image/gif", + jpg: "image/jpeg", + png: "image/png", + svg: "image/svg+xml", + js: "application/javascript", + woff2: "font/woff2", + ttf: "font/ttf" + }; return mimeTypes[path.extname(filename).slice(1)] || "text/plain" } @@ -94,12 +100,6 @@ export class Router { } protected addRoutes() { - const { - BASE: apiBase, - KUBE_HELM: apiHelm, - KUBE_RESOURCE_APPLIER: apiResource, - } = apiPrefix; - // Static assets this.router.add({ method: 'get', path: '/{path*}' }, (request: LensApiRequest) => { const { response, params } = request @@ -107,33 +107,33 @@ export class Router { this.handleStaticFile(file, response) }) - this.router.add({ method: "get", path: `${apiBase}/config` }, configRoute.routeConfig.bind(configRoute)) - this.router.add({ method: "get", path: `${apiBase}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute)) + this.router.add({ method: "get", path: `${apiPrefix}/config` }, configRoute.routeConfig.bind(configRoute)) + this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute)) // Watch API - this.router.add({ method: "get", path: `${apiBase}/watch` }, watchRoute.routeWatch.bind(watchRoute)) + this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute)) // Metrics API - this.router.add({ method: "post", path: `${apiBase}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)) + this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)) // Port-forward API - this.router.add({ method: "post", path: `${apiBase}/services/{namespace}/{service}/port-forward/{port}` }, portForwardRoute.routeServicePortForward.bind(portForwardRoute)) + this.router.add({ method: "post", path: `${apiPrefix}/services/{namespace}/{service}/port-forward/{port}` }, portForwardRoute.routeServicePortForward.bind(portForwardRoute)) // Helm API - this.router.add({ method: "get", path: `${apiHelm}/v2/charts` }, helmApi.listCharts.bind(helmApi)) - this.router.add({ method: "get", path: `${apiHelm}/v2/charts/{repo}/{chart}` }, helmApi.getChart.bind(helmApi)) - this.router.add({ method: "get", path: `${apiHelm}/v2/charts/{repo}/{chart}/values` }, helmApi.getChartValues.bind(helmApi)) + this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmApi.listCharts.bind(helmApi)) + this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmApi.getChart.bind(helmApi)) + this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmApi.getChartValues.bind(helmApi)) - this.router.add({ method: "post", path: `${apiHelm}/v2/releases` }, helmApi.installChart.bind(helmApi)) - this.router.add({ method: `put`, path: `${apiHelm}/v2/releases/{namespace}/{release}` }, helmApi.updateRelease.bind(helmApi)) - this.router.add({ method: `put`, path: `${apiHelm}/v2/releases/{namespace}/{release}/rollback` }, helmApi.rollbackRelease.bind(helmApi)) - this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace?}` }, helmApi.listReleases.bind(helmApi)) - this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace}/{release}` }, helmApi.getRelease.bind(helmApi)) - this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace}/{release}/values` }, helmApi.getReleaseValues.bind(helmApi)) - this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace}/{release}/history` }, helmApi.getReleaseHistory.bind(helmApi)) - this.router.add({ method: "delete", path: `${apiHelm}/v2/releases/{namespace}/{release}` }, helmApi.deleteRelease.bind(helmApi)) + this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmApi.installChart.bind(helmApi)) + this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmApi.updateRelease.bind(helmApi)) + this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmApi.rollbackRelease.bind(helmApi)) + this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmApi.listReleases.bind(helmApi)) + this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmApi.getRelease.bind(helmApi)) + this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmApi.getReleaseValues.bind(helmApi)) + this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmApi.getReleaseHistory.bind(helmApi)) + this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmApi.deleteRelease.bind(helmApi)) // Resource Applier API - this.router.add({ method: "post", path: `${apiResource}/stack` }, resourceApplierApi.applyResource.bind(resourceApplierApi)) + this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierApi.applyResource.bind(resourceApplierApi)) } } diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index 867baf6c86..78ae3735c8 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -2,55 +2,41 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" import requestPromise from "request-promise-native" import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry" -import { apiPrefix } from "../../common/vars"; export type IMetricsQuery = string | string[] | { [metricName: string]: string; } class MetricsRoute extends LensApi { - - public async routeMetrics(request: LensApiRequest) { - const { response, cluster } = request - const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}` // fixme: extract - const query: IMetricsQuery = request.payload; - const headers: Record = { - "Host": `${cluster.id}.localhost:${cluster.port}`, - "Content-type": "application/json", - } - const queryParams: IMetricsQuery = {} - request.query.forEach((value: string, key: string) => { - queryParams[key] = value - }) - + public async routeMetrics(request: LensApiRequest) { + const { response, cluster, payload } = request + const { contextHandler } = cluster; + const serverUrl = await contextHandler.getApiTargetUrl(); let metricsUrl: string let prometheusProvider: PrometheusProvider try { - const prometheusPath = await cluster.contextHandler.getPrometheusPath() + const prometheusPath = await contextHandler.getPrometheusPath() metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` - prometheusProvider = await cluster.contextHandler.getPrometheusProvider() + prometheusProvider = await contextHandler.getPrometheusProvider() } catch { this.respondJson(response, {}) return } // prometheus metrics loader - const attempts: { [query: string]: number } = {}; + const attempts: Record = {}; const maxAttempts = 5; - const loadMetrics = (orgQuery: string): Promise => { - const query = orgQuery.trim() - const attempt = attempts[query] = (attempts[query] || 0) + 1; + const loadMetrics = (promQuery: string): Promise => { + const queryString = request.query.toString() + `&query=` + promQuery; + const attempt = attempts[queryString] = (attempts[queryString] || 0) + 1; return requestPromise(metricsUrl, { - resolveWithFullResponse: false, - headers: headers, json: true, - qs: { - query: query, - ...queryParams - } + qs: queryString, + useQuerystring: true, + resolveWithFullResponse: false, }).catch(async (error) => { if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) { await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request - return loadMetrics(query); + return loadMetrics(queryString); } return { status: error.toString(), @@ -63,14 +49,14 @@ class MetricsRoute extends LensApi { // return data in same structure as query let data: any; - if (typeof query === "string") { - data = await loadMetrics(query) - } else if (Array.isArray(query)) { - data = await Promise.all(query.map(loadMetrics)); + if (typeof payload === "string") { + data = await loadMetrics(payload) + } else if (Array.isArray(payload)) { + data = await Promise.all(payload.map(loadMetrics)); } else { data = {}; const result = await Promise.all( - Object.entries(query).map((queryEntry: any) => { + Object.entries(payload).map((queryEntry: any) => { const queryName: string = queryEntry[0] const queryOpts: PrometheusQueryOpts = queryEntry[1] const queries = prometheusProvider.getQueries(queryOpts) @@ -78,7 +64,7 @@ class MetricsRoute extends LensApi { return loadMetrics(q) }) ); - Object.keys(query).forEach((metricName, index) => { + Object.keys(payload).forEach((metricName, index) => { data[metricName] = result[index]; }); } diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index 1f2d8ce0fd..8943cd49df 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -1,5 +1,5 @@ import { compile } from "path-to-regexp"; -import { apiHelm } from "../index"; +import { apiBase } from "../index"; import { stringify } from "querystring"; import { autobind } from "../../utils"; @@ -21,7 +21,7 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: { export const helmChartsApi = { list() { - return apiHelm + return apiBase .get(endpoint()) .then(data => { return Object @@ -33,7 +33,7 @@ export const helmChartsApi = { get(repo: string, name: string, readmeVersion?: string) { const path = endpoint({ repo, name }); - return apiHelm + return apiBase .get(path + "?" + stringify({ version: readmeVersion })) .then(data => { const versions = data.versions.map(HelmChart.create); @@ -46,7 +46,7 @@ export const helmChartsApi = { }, getValues(repo: string, name: string, version: string) { - return apiHelm + return apiBase .get(`/v2/charts/${repo}/${name}/values?` + stringify({ version })); } }; diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index 831569ef3f..a3c1a62045 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -2,7 +2,7 @@ import jsYaml from "js-yaml"; import { compile } from "path-to-regexp"; import { autobind, formatDuration } from "../../utils"; import capitalize from "lodash/capitalize"; -import { apiHelm } from "../index"; +import { apiBase } from "../index"; import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store"; import { ItemObject } from "../../item.store"; import { KubeObject } from "../kube-object"; @@ -69,14 +69,14 @@ const endpoint = compile(`/v2/releases/:namespace?/:name?`) as ( export const helmReleasesApi = { list(namespace?: string) { - return apiHelm + return apiBase .get(endpoint({ namespace })) .then(releases => releases.map(HelmRelease.create)); }, get(name: string, namespace: string) { const path = endpoint({ name, namespace }); - return apiHelm.get(path).then(details => { + return apiBase.get(path).then(details => { const items: KubeObject[] = JSON.parse(details.resources).items; const resources = items.map(item => KubeObject.create(item)); return { @@ -90,34 +90,34 @@ export const helmReleasesApi = { const { repo, ...data } = payload; data.chart = `${repo}/${data.chart}`; data.values = jsYaml.safeLoad(data.values); - return apiHelm.post(endpoint(), { data }); + return apiBase.post(endpoint(), { data }); }, update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise { const { repo, ...data } = payload; data.chart = `${repo}/${data.chart}`; data.values = jsYaml.safeLoad(data.values); - return apiHelm.put(endpoint({ name, namespace }), { data }); + return apiBase.put(endpoint({ name, namespace }), { data }); }, async delete(name: string, namespace: string) { const path = endpoint({ name, namespace }); - return apiHelm.del(path); + return apiBase.del(path); }, getValues(name: string, namespace: string) { const path = endpoint({ name, namespace }) + "/values"; - return apiHelm.get(path); + return apiBase.get(path); }, getHistory(name: string, namespace: string): Promise { const path = endpoint({ name, namespace }) + "/history"; - return apiHelm.get(path); + return apiBase.get(path); }, rollback(name: string, namespace: string, revision: number) { const path = endpoint({ name, namespace }) + "/rollback"; - return apiHelm.put(path, { + return apiBase.put(path, { data: { revision: revision } diff --git a/src/renderer/api/endpoints/resource-applier.api.ts b/src/renderer/api/endpoints/resource-applier.api.ts index 6697b4a56d..3cecb3de65 100644 --- a/src/renderer/api/endpoints/resource-applier.api.ts +++ b/src/renderer/api/endpoints/resource-applier.api.ts @@ -1,7 +1,7 @@ import jsYaml from "js-yaml" import { KubeObject } from "../kube-object"; import { KubeJsonApiData } from "../kube-json-api"; -import { apiResourceApplier } from "../index"; +import { apiBase } from "../index"; import { apiManager } from "../api-manager"; export const resourceApplierApi = { @@ -13,7 +13,7 @@ export const resourceApplierApi = { if (typeof resource === "string") { resource = jsYaml.safeLoad(resource); } - return apiResourceApplier + return apiBase .post("/stack", { data: resource }) .then(data => { const items = data.map(obj => { diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index 33667f1eab..c2d047b0c8 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -1,25 +1,15 @@ import { JsonApi, JsonApiErrorParsed } from "./json-api"; import { KubeJsonApi } from "./kube-json-api"; import { Notifications } from "../components/notifications"; -import { apiPrefix, isDevelopment } from "../../common/vars"; - -//-- JSON HTTP APIS +import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars"; export const apiBase = new JsonApi({ debug: isDevelopment, - apiPrefix: apiPrefix.BASE, + apiPrefix: apiPrefix, }); export const apiKube = new KubeJsonApi({ debug: isDevelopment, - apiPrefix: apiPrefix.KUBE_BASE, -}); -export const apiHelm = new KubeJsonApi({ - debug: isDevelopment, - apiPrefix: apiPrefix.KUBE_HELM, -}); -export const apiResourceApplier = new KubeJsonApi({ - debug: isDevelopment, - apiPrefix: apiPrefix.KUBE_RESOURCE_APPLIER, + apiPrefix: apiKubePrefix, }); // Common handler for HTTP api errors @@ -34,5 +24,3 @@ function onApiError(error: JsonApiErrorParsed, res: Response) { apiBase.onError.addListener(onApiError); apiKube.onError.addListener(onApiError); -apiHelm.onError.addListener(onApiError); -apiResourceApplier.onError.addListener(onApiError); diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index 869e3a90ea..7075477771 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -3,12 +3,19 @@ import merge from "lodash/merge" import { stringify } from "querystring"; import { IKubeObjectConstructor, KubeObject } from "./kube-object"; -import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api"; +import { KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api"; import { apiKube } from "./index"; import { kubeWatchApi } from "./kube-watch-api"; import { apiManager } from "./api-manager"; import { createApiLink, parseApi } from "./kube-api-parse"; +export interface IKubeObjectRef { + kind: string; + apiVersion: string; + name: string; + namespace?: string; +} + export interface IKubeApiOptions { kind: string; // resource type within api-group, e.g. "Namespace" apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods" diff --git a/src/renderer/api/kube-json-api.ts b/src/renderer/api/kube-json-api.ts index ad7272cd90..ce7da50826 100644 --- a/src/renderer/api/kube-json-api.ts +++ b/src/renderer/api/kube-json-api.ts @@ -31,13 +31,6 @@ export interface KubeJsonApiData extends JsonApiData { }; } -export interface IKubeObjectRef { - kind: string; - apiVersion: string; - name: string; - namespace?: string; -} - export interface KubeJsonApiError extends JsonApiError { code: number; status: string; @@ -49,14 +42,6 @@ export interface KubeJsonApiError extends JsonApiError { }; } -export interface IKubeJsonApiQuery { - watch?: any; - resourceVersion?: string; - timeoutSeconds?: number; - limit?: number; // doesn't work with ?watch - continue?: string; // might be used with ?limit from second request -} - export class KubeJsonApi extends JsonApi { protected parseError(error: KubeJsonApiError | any, res: Response): string[] { const { status, reason, message } = error; diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 9fadfcfd27..0459d8689a 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -29,7 +29,6 @@ export interface IKubeWatchRouteQuery { export class KubeWatchApi { protected evtSource: EventSource; protected onData = new EventEmitter<[IKubeWatchEvent]>(); - protected apiUrl = apiPrefix.BASE + "/watch"; protected subscribers = observable.map(); protected reconnectTimeoutMs = 5000; protected maxReconnectsOnError = 10; @@ -79,7 +78,7 @@ export class KubeWatchApi { return; } const query = this.getQuery(); - const apiUrl = this.apiUrl + "?" + stringify(query); + const apiUrl = `${apiPrefix}/watch?` + stringify(query); this.evtSource = new EventSource(apiUrl); this.evtSource.onmessage = this.onMessage; this.evtSource.onerror = this.onError;