Lens app source code (#119)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2020-03-15 09:52:02 +02:00
1 parent 936cbd53d4
commit 1d0815abd2
797 files changed
+73714 -215

No files matched your search

+48
View File
@@ -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;
}
+5
View File
@@ -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"
+100
View File
@@ -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
}
}
+139
View File
@@ -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;
}
+82
View File
@@ -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;
}