mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
move metrics api to main
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
47d82e4d62
commit
c0fb808365
@ -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<IKubeRequestParams> = {}) {
|
|
||||||
return kubeRequest<KubeJsonApiDataList>({
|
|
||||||
...params,
|
|
||||||
path: "/api/v1/namespaces",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllowedNamespaces(
|
|
||||||
params: Partial<IKubeRequestParams>,
|
|
||||||
fallbackNs = config.KUBERNETES_NAMESPACE,
|
|
||||||
): Promise<string[]> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,7 +8,7 @@ import compression from "compression"
|
|||||||
import helmet from "helmet"
|
import helmet from "helmet"
|
||||||
import morgan from "morgan"
|
import morgan from "morgan"
|
||||||
import { logger } from "../server/utils/logger"
|
import { logger } from "../server/utils/logger"
|
||||||
import { kubewatchRoute, metricsRoute, readyStateRoute } from "../server/routes";
|
import { kubewatchRoute, readyStateRoute } from "../server/routes";
|
||||||
import { useRequestHeaderToken } from "../server/middlewares";
|
import { useRequestHeaderToken } from "../server/middlewares";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -25,7 +25,6 @@ app.set('trust proxy', 1); // trust first proxy
|
|||||||
localApis.use(
|
localApis.use(
|
||||||
readyStateRoute(),
|
readyStateRoute(),
|
||||||
kubewatchRoute(),
|
kubewatchRoute(),
|
||||||
metricsRoute()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// https://github.com/expressjs/cookie-session
|
// https://github.com/expressjs/cookie-session
|
||||||
|
|||||||
@ -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<IMetrics> => {
|
|
||||||
const attempt = attempts[query] = (attempts[query] || 0) + 1;
|
|
||||||
return kubeRequest<IMetrics>({
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@ -86,6 +86,10 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPrometheusPath() {
|
||||||
|
return this.prometheusPath
|
||||||
|
}
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
const currentCluster = this.kc.getCurrentCluster()
|
const currentCluster = this.kc.getCurrentCluster()
|
||||||
if (currentCluster.caFile) {
|
if (currentCluster.caFile) {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { configRoute } from "./routes/config"
|
|||||||
import { helmApi } from "./helm-api"
|
import { helmApi } from "./helm-api"
|
||||||
import { resourceApplierApi } from "./resource-applier-api"
|
import { resourceApplierApi } from "./resource-applier-api"
|
||||||
import { kubeconfigRoute } from "./routes/kubeconfig"
|
import { kubeconfigRoute } from "./routes/kubeconfig"
|
||||||
|
import { metricsRoute } from "./routes/metrics"
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Call = require('@hapi/call');
|
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/config' }, configRoute.routeConfig.bind(configRoute))
|
||||||
this.router.add({ method: 'get', path: '/api/kubeconfig/service-account/{namespace}/{account}' }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
|
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
|
// 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' }, helmApi.listCharts.bind(helmApi))
|
||||||
this.router.add({ method: 'get', path: '/api-helm/v2/charts/{repo}/{chart}' }, helmApi.getChart.bind(helmApi))
|
this.router.add({ method: 'get', path: '/api-helm/v2/charts/{repo}/{chart}' }, helmApi.getChart.bind(helmApi))
|
||||||
|
|||||||
75
src/main/routes/metrics.ts
Normal file
75
src/main/routes/metrics.ts
Normal file
@ -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<any> => {
|
||||||
|
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()
|
||||||
Loading…
Reference in New Issue
Block a user