diff --git a/dashboard/server/api/get-namespaces.ts b/dashboard/server/api/get-namespaces.ts deleted file mode 100644 index 4aad15a6b3..0000000000 --- a/dashboard/server/api/get-namespaces.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Get namespaces - -import config from "../config"; -import { KubeJsonApiDataList } from "../../client/api/kube-json-api"; -import { IKubeRequestParams, kubeRequest } from "./kube-request"; -import { reviewResourceAccess } from "./review-resource-access"; -import { getServiceAccountToken } from "./get-service-account-token" - -export async function getNamespaces(params: Partial = {}) { - return kubeRequest({ - ...params, - path: "/api/v1/namespaces", - }); -} - -export async function getAllowedNamespaces( - params: Partial, - fallbackNs = config.KUBERNETES_NAMESPACE, -): Promise { - try { - const allNamespaces = await getNamespaces(params); - const nsAccessStatuses = await Promise.all( - allNamespaces.items.map(ns => { - const { name } = ns.metadata; - return reviewResourceAccess(params, { - namespace: name, - resource: "pods", - verb: "list", - }); - }) - ); - return allNamespaces.items - .filter((ns, i) => nsAccessStatuses[i].allowed) - .map(ns => ns.metadata.name); - } catch (e) { - const serviceToken = await getServiceAccountToken(); - if (!serviceToken) { - return fallbackNs ? [fallbackNs] : []; - } - // fetch namespaces with service-account token (cluster-wide) - // and for every namespace make additional request to check if namespace available for user-token - const allNamespaces = await getNamespaces({ - authHeader: `Bearer ${serviceToken}` - }); - const nsAccessStatuses = await Promise.all( - allNamespaces.items.map(ns => { - const { name } = ns.metadata; - return reviewResourceAccess(params, { - namespace: name, - resource: "pods", - verb: "list", - }); - }) - ); - return allNamespaces.items - .filter((ns, i) => nsAccessStatuses[i].allowed) - .map(ns => ns.metadata.name); - } -} diff --git a/dashboard/server/app.ts b/dashboard/server/app.ts index 9f6d98c58c..34da111c48 100644 --- a/dashboard/server/app.ts +++ b/dashboard/server/app.ts @@ -8,7 +8,7 @@ import compression from "compression" import helmet from "helmet" import morgan from "morgan" import { logger } from "../server/utils/logger" -import { kubewatchRoute, metricsRoute, readyStateRoute } from "../server/routes"; +import { kubewatchRoute, readyStateRoute } from "../server/routes"; import { useRequestHeaderToken } from "../server/middlewares"; const { @@ -25,7 +25,6 @@ app.set('trust proxy', 1); // trust first proxy localApis.use( readyStateRoute(), kubewatchRoute(), - metricsRoute() ); // https://github.com/expressjs/cookie-session diff --git a/dashboard/server/routes/metrics-route.ts b/dashboard/server/routes/metrics-route.ts deleted file mode 100644 index 8234432098..0000000000 --- a/dashboard/server/routes/metrics-route.ts +++ /dev/null @@ -1,82 +0,0 @@ -//-- Metrics -// https://prometheus.io/docs/prometheus/latest/querying/api/ - -import { Router } from "express"; -import config from "../config"; -import { kubeRequest } from "../api/kube-request"; -import { userSession } from "../user-session"; -import { AxiosError } from "axios"; -import { IMetrics } from "../../client/api/endpoints/metrics.api"; -import { IMetricsQuery } from "../common/metrics" - - -export function metricsRoute() { - const router = Router(); - - router.post("/metrics", async (req, res, next) => { - const { authHeader } = userSession.get(req); - const { namespace, ...queryParams } = req.query; - const query: IMetricsQuery = req.body; - - /*eslint-disable */ - // add default namespace for rbac-proxy validation - if (!queryParams.kubernetes_namespace) { - queryParams.kubernetes_namespace = config.STATS_NAMESPACE; - } - /*eslint-enble */ - - // prometheus metrics loader - const attempts: { [query: string]: number } = {}; - const maxAttempts = 5; - const loadMetrics = (query: string): Promise => { - const attempt = attempts[query] = (attempts[query] || 0) + 1; - return kubeRequest({ - url: config.KUBE_METRICS_URL, - path: "/api/v1/query_range", - authHeader: authHeader, - params: { - query: query, - ...queryParams, - }, - }).catch(async (err: AxiosError) => { - // https://github.com/axios/axios#handling-errors - if (!err.response && attempt < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request - return loadMetrics(query); - } - return { - status: err.toString(), - data: { - result: [] - }, - } as IMetrics; - }) - }; - - // return data in same structure as query - let data: any; - try { - if (typeof query === "string") { - data = await loadMetrics(query) - } - else if (Array.isArray(query)) { - data = await Promise.all(query.map(loadMetrics)); - } - else { - data = {}; - const result = await Promise.all( - Object.values(query).map(loadMetrics) - ); - Object.keys(query).forEach((metricName, index) => { - data[metricName] = result[index]; - }); - } - - res.json(data); - } catch (err) { - next(err); - } - }); - - return router; -} diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index 80e14474c6..9d321e9591 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -86,6 +86,10 @@ export class ContextHandler { } } + public getPrometheusPath() { + return this.prometheusPath + } + public async init() { const currentCluster = this.kc.getCurrentCluster() if (currentCluster.caFile) { diff --git a/src/main/router.ts b/src/main/router.ts index 39b3d559f1..3dba1ad865 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -4,6 +4,7 @@ import { configRoute } from "./routes/config" import { helmApi } from "./helm-api" import { resourceApplierApi } from "./resource-applier-api" import { kubeconfigRoute } from "./routes/kubeconfig" +import { metricsRoute } from "./routes/metrics" // eslint-disable-next-line @typescript-eslint/no-var-requires const Call = require('@hapi/call'); @@ -70,6 +71,9 @@ export class Router { this.router.add({ method: 'get', path: '/api/config' }, configRoute.routeConfig.bind(configRoute)) this.router.add({ method: 'get', path: '/api/kubeconfig/service-account/{namespace}/{account}' }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute)) + // Metrics API + this.router.add({ method: 'post', path: '/api/metrics' }, metricsRoute.routeMetrics.bind(metricsRoute)) + // Helm API this.router.add({ method: 'get', path: '/api-helm/v2/charts' }, helmApi.listCharts.bind(helmApi)) this.router.add({ method: 'get', path: '/api-helm/v2/charts/{repo}/{chart}' }, helmApi.getChart.bind(helmApi)) diff --git a/src/main/routes/metrics.ts b/src/main/routes/metrics.ts new file mode 100644 index 0000000000..51940956c6 --- /dev/null +++ b/src/main/routes/metrics.ts @@ -0,0 +1,75 @@ +import { LensApiRequest } from "../router" +import { LensApi } from "../lens-api" +import * as requestPromise from "request-promise-native" + +type MetricsQuery = string | string[] | { + [metricName: string]: string; +} + +class MetricsRoute extends LensApi { + + public async routeMetrics(request: LensApiRequest) { + const { response, cluster} = request + const query: MetricsQuery = request.payload; + const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube` + const metricsUrl = `${serverUrl}/api/v1/namespaces/${cluster.contextHandler.getPrometheusPath()}/proxy/api/v1/query_range` + const headers = { + "Host": `${cluster.id}.localhost:${cluster.port}`, + "Content-type": "application/json", + } + const queryParams: MetricsQuery = {} + request.query.forEach((value: string, key: string) => { + queryParams[key] = value + }) + + // prometheus metrics loader + const attempts: { [query: string]: number } = {}; + const maxAttempts = 5; + const loadMetrics = (orgQuery: string): Promise => { + const query = orgQuery.trim() + const attempt = attempts[query] = (attempts[query] || 0) + 1; + return requestPromise(metricsUrl, { + resolveWithFullResponse: false, + headers: headers, + json: true, + qs: { + query: query, + ...queryParams + } + }).catch(async (error) => { + if (attempt < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request + return loadMetrics(query); + } + return { + status: error.toString(), + data: { + result: [] + } + } + }) + }; + + // 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)); + } + else { + data = {}; + const result = await Promise.all( + Object.values(query).map(loadMetrics) + ); + Object.keys(query).forEach((metricName, index) => { + data[metricName] = result[index]; + }); + } + + this.respondJson(response, data) + } +} + +export const metricsRoute = new MetricsRoute()