mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Lens app source code (#119)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
936cbd53d4
commit
1d0815abd2
797 files changed
+73714
-215
No files matched your search
@@ -0,0 +1,48 @@
|
||||
//-- Config route
|
||||
|
||||
import config from "../config";
|
||||
import { IConfig } from "../common/config"
|
||||
import { Router } from "express";
|
||||
import { userSession } from "../user-session";
|
||||
import { getClusterInfo } from "../api/get-cluster-info";
|
||||
import { isClusterAdmin } from "../api/is-cluster-admin";
|
||||
import { getAllowedNamespaces } from "../api/get-namespaces";
|
||||
import { parseJwt } from "../utils/parse-jwt";
|
||||
|
||||
export function configRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/config')
|
||||
.get(async (req, res) => {
|
||||
const { username, authHeader } = userSession.get(req);
|
||||
const authToken = userSession.getToken(req);
|
||||
|
||||
const data: IConfig = {
|
||||
clusterName: config.KUBE_CLUSTER_NAME,
|
||||
lensVersion: config.LENS_VERSION,
|
||||
lensTheme: config.LENS_THEME,
|
||||
chartsEnabled: !!config.CHARTS_ENABLED,
|
||||
kubectlAccess: !!req.headers["x-lens-kubectl-token"]
|
||||
};
|
||||
|
||||
// load config data from other places
|
||||
const loading: Promise<any>[] = [
|
||||
getClusterInfo().then(info => Object.assign(data, info)),
|
||||
];
|
||||
|
||||
// validate user token from session and fetch more config data
|
||||
if (authToken) {
|
||||
const { sub, email } = parseJwt(authToken);
|
||||
data.username = email || sub || username;
|
||||
data.token = authToken;
|
||||
loading.push(
|
||||
isClusterAdmin({ authHeader }).then(isAdmin => data.isClusterAdmin = isAdmin),
|
||||
getAllowedNamespaces({ authHeader }).then(list => data.allowedNamespaces = list),
|
||||
);
|
||||
}
|
||||
await Promise.all(loading);
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./config-route"
|
||||
export * from "./kubeconfig-route"
|
||||
export * from "./kubewatch-route"
|
||||
export * from "./metrics-route"
|
||||
export * from "./ready-state-route"
|
||||
@@ -0,0 +1,100 @@
|
||||
//-- Kubeconfig route
|
||||
|
||||
import { Router } from "express";
|
||||
import { AxiosError } from "axios";
|
||||
import { userSession } from "../user-session";
|
||||
import { kubeRequest } from "../api/kube-request";
|
||||
import { Secret } from "../../client/api/endpoints";
|
||||
import { base64 } from "../../client/utils/base64";
|
||||
import { getCertificateAuthorityData } from "../api/get-cert-auth-data";
|
||||
import { getClusterConfigMap } from "../api/get-cluster-info";
|
||||
|
||||
interface IKubeConfigParams {
|
||||
username: string;
|
||||
authToken: string;
|
||||
certificateAuthority: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export function kubeconfigRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/kubeconfig/user')
|
||||
.get(async (req, res) => {
|
||||
const { username = "kubernetes" } = userSession.get(req);
|
||||
const authToken = userSession.getToken(req);
|
||||
const cert = await getCertificateAuthorityData('base64');
|
||||
const data = await generateKubeConfig({
|
||||
username,
|
||||
authToken: authToken,
|
||||
certificateAuthority: cert
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
router.route('/kubeconfig/service-account/:namespace/:account')
|
||||
.get(async (req, res) => {
|
||||
const { authHeader } = userSession.get(req);
|
||||
const { namespace, account } = req.params;
|
||||
try {
|
||||
const secret = await kubeRequest<{ items: Secret[] }>({
|
||||
path: `/api/v1/namespaces/${namespace}/secrets`,
|
||||
authHeader: authHeader,
|
||||
}).then(secrets => {
|
||||
return secrets.items.find(secret => {
|
||||
const { annotations } = secret.metadata;
|
||||
return annotations && annotations["kubernetes.io/service-account.name"] == account;
|
||||
});
|
||||
});
|
||||
const data = await generateKubeConfig({
|
||||
username: account,
|
||||
namespace: namespace,
|
||||
authToken: base64.decode(secret.data.token),
|
||||
certificateAuthority: secret.data["ca.crt"],
|
||||
});
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
const { response }: AxiosError = err;
|
||||
res.status(403).json(response ? response.data : err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function generateKubeConfig(params: IKubeConfigParams) {
|
||||
const { clusterName, clusterUrl } = await getClusterConfigMap();
|
||||
const { authToken, username, certificateAuthority, namespace = "" } = params;
|
||||
return {
|
||||
'apiVersion': 'v1',
|
||||
'kind': 'Config',
|
||||
'clusters': [
|
||||
{
|
||||
'name': clusterName,
|
||||
'cluster': {
|
||||
'server': clusterUrl,
|
||||
'certificate-authority-data': certificateAuthority
|
||||
}
|
||||
}
|
||||
],
|
||||
'users': [
|
||||
{
|
||||
'name': username,
|
||||
'user': {
|
||||
'token': authToken,
|
||||
}
|
||||
}
|
||||
],
|
||||
'contexts': [
|
||||
{
|
||||
'name': clusterName,
|
||||
'context': {
|
||||
'user': username,
|
||||
'cluster': clusterName,
|
||||
'namespace': namespace,
|
||||
}
|
||||
}
|
||||
],
|
||||
'current-context': clusterName
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//-- Streaming k8s watch-api events
|
||||
|
||||
import axios from "axios"
|
||||
import { Router } from "express";
|
||||
import { IncomingMessage } from "http";
|
||||
import { kubeRequest } from "../api/kube-request";
|
||||
import { IKubeWatchEvent, IKubeWatchRouteEvent, IKubeWatchRouteQuery} from "../common/kubewatch"
|
||||
import { userSession } from "../user-session";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export function kubewatchRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/watch')
|
||||
.get(async (req, res) => {
|
||||
const { authHeader } = userSession.get(req);
|
||||
const queryParams: IKubeWatchRouteQuery = req.query;
|
||||
const apis: string[] = [].concat(queryParams.api || []);
|
||||
const streams = new Map<string, IncomingMessage>();
|
||||
const eventsBuffer = new Map<string, IKubeWatchEvent>();
|
||||
let isClosing = false;
|
||||
|
||||
if (!apis.length) {
|
||||
res.status(400).json({
|
||||
message: "Empty request. Query params 'api' are not provided.",
|
||||
example: "?api=/api/v1/pods&api=/api/v1/nodes",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.header({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
});
|
||||
|
||||
// init streams
|
||||
const cancelToken = axios.CancelToken.source();
|
||||
apis.forEach(apiUrl => {
|
||||
console.log("[KUBE-WATCH] init stream", apiUrl);
|
||||
const connecting = kubeRequest<IncomingMessage>({
|
||||
path: apiUrl,
|
||||
responseType: "stream",
|
||||
authHeader: authHeader,
|
||||
cancelToken: cancelToken.token,
|
||||
});
|
||||
connecting.then(stream => {
|
||||
streams.set(apiUrl, stream); // save connection for clean up
|
||||
stream.socket.setKeepAlive(true); // keep connection alive
|
||||
let lastUnusedBuffer = ""
|
||||
return stream
|
||||
.on("data", (buffer: Buffer) => {
|
||||
const data = lastUnusedBuffer + buffer.toString().trim();
|
||||
data.split("\n").map(str => {
|
||||
try {
|
||||
const eventObj = JSON.parse(str);
|
||||
bufferEvent(eventObj); // handle
|
||||
lastUnusedBuffer = ""; // clean up since parsing was successful
|
||||
} catch (err) {
|
||||
lastUnusedBuffer = str; // invalid json, tail must wait next incoming data
|
||||
}
|
||||
});
|
||||
})
|
||||
.on("end", () => {
|
||||
// client must update resource-version and try to reconnect
|
||||
console.log(`[KUBE-WATCH] stream ended ${apiUrl}`)
|
||||
sendEvent({
|
||||
type: "STREAM_END",
|
||||
url: apiUrl,
|
||||
status: stream.statusCode,
|
||||
})
|
||||
});
|
||||
}, err => {
|
||||
logger.error(`[KUBE-WATCH] error ${apiUrl}`, err);
|
||||
sendEvent({
|
||||
type: "STREAM_END",
|
||||
url: apiUrl,
|
||||
status: 410,
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
function getEventBufferId(evt: IKubeWatchEvent) {
|
||||
const { object, type } = evt;
|
||||
const { kind } = object;
|
||||
let { metadata: { uid } } = object;
|
||||
if (kind === "Event") {
|
||||
uid = (object as any).involvedObject.uid; // reason: uid for events always unique
|
||||
}
|
||||
return `${type}:${kind}-${uid}`
|
||||
}
|
||||
|
||||
function bufferEvent(evt: IKubeWatchEvent) {
|
||||
const id = getEventBufferId(evt);
|
||||
if (eventsBuffer.has(id)) {
|
||||
eventsBuffer.delete(id); // clear to move event to the end in map's "timeline"
|
||||
}
|
||||
eventsBuffer.set(id, evt); // save latest event by object's identity
|
||||
}
|
||||
|
||||
function sendEvent(evt: IKubeWatchEvent | IKubeWatchRouteEvent, autoFlush = true) {
|
||||
if (isClosing) return;
|
||||
// convert to "text/event-stream" format
|
||||
res.write(`data: ${JSON.stringify(evt)}\n\n`);
|
||||
if (autoFlush) {
|
||||
// @ts-ignore
|
||||
res.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// process sending events
|
||||
const flushInterval = setInterval(() => {
|
||||
const eventsPack = Array.from(eventsBuffer.entries())
|
||||
.slice(0, 100) // max limit per sending
|
||||
.map(([id, evt]) => {
|
||||
eventsBuffer.delete(id); // clean up used event
|
||||
return evt;
|
||||
});
|
||||
if (eventsPack.length > 0) {
|
||||
eventsPack.forEach(evt => sendEvent(evt, false));
|
||||
// @ts-ignore
|
||||
res.flush();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
function onClose() {
|
||||
if (isClosing) return;
|
||||
isClosing = true;
|
||||
clearInterval(flushInterval);
|
||||
streams.forEach(stream => stream.removeAllListeners("end"));
|
||||
cancelToken.cancel();
|
||||
}
|
||||
|
||||
req.on("close", onClose);
|
||||
res.on("finish", onClose);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//-- 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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//-- App readiness checker
|
||||
|
||||
import { Router } from "express";
|
||||
|
||||
export function readyStateRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/ready')
|
||||
.get(async (req, res) => {
|
||||
const serviceWaitingList: string[] = [];
|
||||
|
||||
res.json(serviceWaitingList);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in new issue
Block a user