1
0
mirror of https://github.com/lensapp/lens.git synced 2024-12-11 09:03:28 +03:00

Refactor dashboard server logic to electron main (#146)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2020-03-24 07:37:47 +02:00 committed by GitHub
parent e54b79b56a
commit 1aaa695cfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 430 additions and 1323 deletions

View File

@ -6,7 +6,7 @@ endif
.PHONY: dev build test clean .PHONY: dev build test clean
dev: app-deps dashboard-deps build-dashboard-server dev: app-deps dashboard-deps
yarn dev yarn dev
test: test-app test-dashboard test: test-app test-dashboard
@ -39,25 +39,8 @@ clean-dashboard:
test-dashboard: dashboard-deps test-dashboard: dashboard-deps
cd dashboard && yarn test cd dashboard && yarn test
build-dashboard: build-dashboard-server build-dashboard-client build-dashboard: dashboard-deps clean-dashboard
cd dashboard && yarn build
build-dashboard-server: dashboard-deps clean-dashboard
cd dashboard && yarn build-server
ifeq "$(DETECTED_OS)" "Linux"
rm binaries/server/linux/lens-server || true
cd dashboard && yarn pkg-server-linux
endif
ifeq "$(DETECTED_OS)" "Darwin"
rm binaries/server/darwin/lens-server || true
cd dashboard && yarn pkg-server-macos
endif
ifeq "$(DETECTED_OS)" "Windows"
rm binaries/server/windows/*.exe || true
cd dashboard && yarn pkg-server-win
endif
build-dashboard-client: dashboard-deps clean-dashboard
cd dashboard && yarn build-client
clean: clean:
rm -rf dist/* rm -rf dist/*

View File

@ -14,6 +14,7 @@ import { UserManagement } from "./+user-management/user-management";
import { ConfirmDialog } from "./confirm-dialog"; import { ConfirmDialog } from "./confirm-dialog";
import { usersManagementRoute } from "./+user-management/user-management.routes"; import { usersManagementRoute } from "./+user-management/user-management.routes";
import { clusterRoute, clusterURL } from "./+cluster"; import { clusterRoute, clusterURL } from "./+cluster";
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
import { Nodes, nodesRoute } from "./+nodes"; import { Nodes, nodesRoute } from "./+nodes";
import { Workloads, workloadsRoute, workloadsURL } from "./+workloads"; import { Workloads, workloadsRoute, workloadsURL } from "./+workloads";
import { Namespaces, namespacesRoute } from "./+namespaces"; import { Namespaces, namespacesRoute } from "./+namespaces";
@ -70,6 +71,7 @@ class App extends React.Component {
<KubeObjectDetails/> <KubeObjectDetails/>
<Notifications/> <Notifications/>
<ConfirmDialog/> <ConfirmDialog/>
<KubeConfigDialog/>
<AddRoleBindingDialog/> <AddRoleBindingDialog/>
<PodLogsDialog/> <PodLogsDialog/>
<DeploymentScaleDialog/> <DeploymentScaleDialog/>

View File

@ -1,31 +1,15 @@
{ {
"name": "kontena-lens", "name": "kontena-lens",
"version": "1.13.1", "version": "0.0.0",
"scripts": { "scripts": {
"app-dev": "yarn extract && webpack-cli --watch --cache --progress --output-path ../static/build/client/", "dev": "yarn extract && webpack-cli --watch --cache --progress --output-path ../static/build/client/",
"build-client": "yarn extract && NODE_ENV=production webpack -p --cache --progress --output-path ../static/build/client/", "build": "yarn extract && NODE_ENV=production webpack -p --cache --progress --output-path ../static/build/client/",
"build-server": "tsc --project server",
"pkg-server-linux": "pkg --no-bytecode --public --public-packages '*' -t node12-linux -o ../binaries/server/linux/lens-server build/server/app.js",
"pkg-server-macos": "pkg --no-bytecode --public --public-packages '*' -t node12-macos -o ../binaries/server/darwin/lens-server build/server/app.js",
"pkg-server-win": "pkg --no-bytecode --public --public-packages '*' -t node12-win -o ../binaries/server/win32/lens-server-x64.exe build/server/app.js",
"test": "jest --config './test/jest.config.js'", "test": "jest --config './test/jest.config.js'",
"add-locale": "lingui add-locale", "add-locale": "lingui add-locale",
"extract": "lingui extract --clean", "extract": "lingui extract --clean",
"compile": "lingui compile" "compile": "lingui compile"
}, },
"nodemonConfig": {
"watch": [
".env",
"package.json",
"server"
],
"ext": "ts",
"execMap": {
"ts": "ts-node --project server/tsconfig.json"
}
},
"dependencies": { "dependencies": {
"@types/jsonpath": "^0.2.0",
"axios": "^0.19.0", "axios": "^0.19.0",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"compression": "^1.7.4", "compression": "^1.7.4",
@ -33,9 +17,6 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto-js": "^3.1.9-1", "crypto-js": "^3.1.9-1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1",
"helmet": "^3.21.2",
"http-proxy-middleware": "^0.20.0",
"ip": "^1.1.5", "ip": "^1.1.5",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"jsonpath": "^1.0.2", "jsonpath": "^1.0.2",
@ -76,6 +57,7 @@
"@types/http-proxy-middleware": "^0.19.3", "@types/http-proxy-middleware": "^0.19.3",
"@types/ip": "^1.1.0", "@types/ip": "^1.1.0",
"@types/jest": "^24.0.22", "@types/jest": "^24.0.22",
"@types/jsonpath": "^0.2.0",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/lingui__macro": "^2.7.3", "@types/lingui__macro": "^2.7.3",
"@types/lodash": "^4.14.146", "@types/lodash": "^4.14.146",

View File

@ -1,23 +0,0 @@
// Get certificate auth data
import * as fs from "fs";
import * as util from "util";
import config from "../config";
let caData: string = null
export async function getCertificateAuthorityData(encoding = 'utf8'): Promise<string> {
if (caData) {
return caData
}
if (!fs.existsSync(config.KUBERNETES_CA_CERT)) {
caData = config.KUBERNETES_CA_CERT
return caData
}
try {
const ca = await util.promisify(fs.readFile)(config.KUBERNETES_CA_CERT);
return Buffer.from(ca).toString(encoding);
} catch (error) {
return ''
}
}

View File

@ -1,39 +0,0 @@
// Get cluster info
import config from "../config"
import { kubeRequest } from "./kube-request";
import { IClusterInfo, IClusterConfigMap } from "../common/cluster"
export async function getClusterInfo(): Promise<IClusterInfo> {
const [configMap, kubeVersion, pharosVersion] = await Promise.all([
getClusterConfigMap().catch(() => ({} as IClusterConfigMap)),
getKubeVersion().catch(() => null),
getPharosVersion().catch(() => null),
]);
return {
...configMap,
kubeVersion,
pharosVersion,
};
}
export async function getClusterConfigMap() {
const res = await kubeRequest<{ data: IClusterConfigMap }>({
path: `/api/v1/namespaces/${config.LENS_NAMESPACE}/configmaps/config`,
});
return res.data;
}
export async function getKubeVersion() {
const res = await kubeRequest<{ gitVersion: string }>({
path: "/version",
});
return res.gitVersion.slice(1);
}
export async function getPharosVersion() {
const res = await kubeRequest<{ data: { "pharos-version": string } }>({
path: `/api/v1/namespaces/kube-system/configmaps/pharos-config`,
});
return res ? res.data["pharos-version"] : null;
}

View File

@ -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);
}
}

View File

@ -1,22 +0,0 @@
// Get service-account token
import { existsSync, readFile } from "fs";
import { promisify } from "util";
import config from "../config"
const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
export async function getServiceAccountToken() {
const { SERVICE_ACCOUNT_TOKEN } = config;
if (SERVICE_ACCOUNT_TOKEN) {
return SERVICE_ACCOUNT_TOKEN;
}
if (existsSync(tokenPath)) {
const token = await promisify(readFile)(tokenPath);
return token.toString().trim();
}
return null;
}

View File

@ -1,19 +0,0 @@
// Check cluster-admin rights for auth-token
// CLI: kubectl auth can-i '*' '*' --all-namespaces
import { reviewResourceAccess } from "./review-resource-access";
import { IKubeRequestParams } from "./kube-request";
export async function isClusterAdmin(params: Partial<IKubeRequestParams>): Promise<boolean> {
try {
const accessCheck = await reviewResourceAccess(params, {
resource: "*",
namespace: "*",
group: "*",
verb: "*",
});
return accessCheck.allowed;
} catch (err) {
return false;
}
}

View File

@ -1,58 +0,0 @@
// Kubernetes request api helper
import config, { isSecure } from "../config";
import axios, { AxiosError, AxiosRequestConfig } from "axios"
import * as https from "https";
import { getCertificateAuthorityData } from "./get-cert-auth-data";
import { logger, sanitizeHeaders } from "../utils/logger";
import { getServiceAccountToken } from "./get-service-account-token";
export interface IKubeRequestParams extends AxiosRequestConfig {
path: string;
authHeader?: string;
}
export async function kubeRequest<T>(params: IKubeRequestParams): Promise<T> {
const { KUBE_CLUSTER_URL, KUBERNETES_CLIENT_CERT, KUBERNETES_CLIENT_KEY } = config;
const serviceToken = await getServiceAccountToken();
const defaultAuthHeader = serviceToken ? `Bearer ${serviceToken}` : "";
const {
authHeader = defaultAuthHeader,
url = KUBE_CLUSTER_URL,
path = "",
...reqConfig
} = params;
// add access token
reqConfig.headers = Object.assign({}, reqConfig.headers, {
"Content-type": "application/json",
});
if (!KUBERNETES_CLIENT_CERT && authHeader) {
reqConfig.headers["Authorization"] = authHeader;
}
// allow requests to kube-cluster without valid ssl certs..
reqConfig.httpsAgent = new https.Agent({
rejectUnauthorized: isSecure(),
cert: KUBERNETES_CLIENT_CERT,
key: KUBERNETES_CLIENT_KEY,
ca: await getCertificateAuthorityData(),
});
const reqUrl = url + path;
return axios(reqUrl, reqConfig)
.then(res => res.data)
.catch((error: AxiosError<T>) => {
const { message, config } = error;
logger.error(`[KUBE-REQUEST]: ${message}`, {
code: error.code,
method: config.method,
url: config.url,
headers: sanitizeHeaders(config.headers),
params: config.params,
});
throw error;
});
}

View File

@ -1,47 +0,0 @@
// Get resource access review
// Docs: https://kubernetes.io/docs/reference/access-authn-authz/authorization/
import { IKubeRequestParams, kubeRequest } from "./kube-request";
interface IResourceAccess {
apiVersion: string;
kind: string;
status: IResourceAccessStatus;
}
export interface IResourceAccessStatus {
allowed: boolean;
denied?: boolean;
reason?: string;
evaluationError?: string;
}
interface IResourceAccessAttributes {
group?: string | "*";
resource?: string | "*";
verb?: "get" | "list" | "create" | "update" | "patch" | "watch" | "proxy" | "redirect" | "delete" | "deletecollection" | "*";
namespace?: string | "*";
}
export async function reviewResourceAccess(
params: Partial<IKubeRequestParams> = {},
attrs: IResourceAccessAttributes
): Promise<IResourceAccessStatus> {
try {
const accessReview = await kubeRequest<IResourceAccess>({
...params,
method: "POST",
path: "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews",
data: {
spec: {
resourceAttributes: attrs
}
}
});
return accessReview.status;
} catch (err) {
return {
allowed: false,
reason: err.toString(),
}
}
}

View File

@ -1,39 +0,0 @@
// Check validity of auth-token
import { kubeRequest } from "./kube-request";
export interface ITokenReview {
apiVersion: string;
kind: string;
status: ITokenReviewStatus;
}
export interface ITokenReviewStatus {
authenticated: boolean;
user: {
username?: string;
uid?: string;
groups?: string[];
};
error?: string[];
}
export async function reviewToken(authToken: string): Promise<ITokenReviewStatus> {
try {
const tokenReview = await kubeRequest<ITokenReview>({
path: "/apis/authentication.k8s.io/v1/tokenreviews",
method: "POST",
data: {
spec: {
token: authToken
}
}
});
return tokenReview.status;
} catch (err) {
return {
authenticated: false,
user: {},
error: [err.toString()],
}
}
}

View File

@ -1,76 +0,0 @@
import config, { BUILD_DIR, CLIENT_DIR } from "../server/config"
import path from "path"
import fs from "fs"
import express from "express"
import cookieSession from "cookie-session"
import compression from "compression"
import helmet from "helmet"
import morgan from "morgan"
import { logger } from "../server/utils/logger"
import { configRoute, kubeconfigRoute, kubewatchRoute, metricsRoute, readyStateRoute } from "../server/routes";
import { useRequestHeaderToken } from "../server/middlewares";
const {
IS_PRODUCTION, LOCAL_SERVER_PORT, API_PREFIX,
SESSION_NAME, SESSION_SECRET,
} = config;
const app = express();
const localApis = express.Router();
const outputDir = path.resolve(process.cwd(), BUILD_DIR, CLIENT_DIR);
app.set('trust proxy', 1); // trust first proxy
localApis.use(
configRoute(),
readyStateRoute(),
kubeconfigRoute(),
kubewatchRoute(),
metricsRoute()
);
// https://github.com/expressjs/cookie-session
app.use(cookieSession({
name: SESSION_NAME,
secret: SESSION_SECRET,
secure: IS_PRODUCTION,
httpOnly: true,
maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
}));
// protect from well-known web vulnerabilities by setting HTTP headers appropriately
// https://github.com/helmetjs/helmet
app.use(helmet({
hsts: {
includeSubDomains: false,
}
}));
// use auth-token from request headers (if applicable via proxy)
app.use(useRequestHeaderToken());
// requests logging
app.use(morgan('tiny'));
// enable gzip compression
app.use(compression());
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
app.use("/", express.static(outputDir)); // handle static files (assets)
app.use(API_PREFIX.BASE, express.json({ limit: "10mb" }), localApis);
// handle all page requests via index.html, in development mode it's managed by webpack-dev-server
app.all('*', (req, res) => {
const indexHtml = path.resolve(outputDir, 'index.html');
if (fs.existsSync(indexHtml)) res.sendFile(indexHtml);
else {
res.send("Error: build/index.html doesn't exists");
}
});
// run server
const server = app.listen(LOCAL_SERVER_PORT, "127.0.0.1", () => {
logger.appStarted(LOCAL_SERVER_PORT, 'Server started');
});

View File

@ -1,9 +1,4 @@
export interface IClusterConfigMap { export interface IClusterInfo {
clusterName: string;
clusterUrl: string;
}
export interface IClusterInfo extends IClusterConfigMap {
kubeVersion?: string; kubeVersion?: string;
pharosVersion?: string; clusterName?: string;
} }

View File

@ -1,3 +0,0 @@
export * from "./kube-proxy"
export * from "./terminal-proxy"
export * from "./use-header-token"

View File

@ -1,24 +0,0 @@
import { Request } from "express";
import proxy from "http-proxy-middleware"
import { userSession } from "../user-session";
import config, { isSecure } from "../config";
export function kubeProxy(serviceUrl: string, proxyConfig: proxy.Config = {}) {
const { IS_PRODUCTION } = config;
return proxy({
target: serviceUrl,
secure: isSecure(), // verify the ssl certs
logLevel: IS_PRODUCTION ? "info" : "debug",
changeOrigin: true, // needed for virtual hosted sites
pathRewrite: (path, req: Request) => {
return path.replace(req.baseUrl, ""); // remove client-prefix, e.g "/api-kube"
},
onProxyReq(proxyReq, req: Request, res) {
const { authHeader } = userSession.get(req);
if (authHeader) {
proxyReq.setHeader("Authorization", authHeader);
}
},
...proxyConfig,
})
}

View File

@ -1,19 +0,0 @@
import { NextFunction } from "express";
import proxy from "http-proxy-middleware"
import appConfig from "../config"
const { KUBE_TERMINAL_URL, API_PREFIX, IS_PRODUCTION } = appConfig;
interface ITerminalProxy extends NextFunction {
upgrade: () => void;
}
export const terminalProxy = proxy({
target: KUBE_TERMINAL_URL,
ws: true,
changeOrigin: true,
logLevel: IS_PRODUCTION ? "info" : "debug",
pathRewrite: {
["^" + API_PREFIX.TERMINAL]: "" // remove api-prefix
}
}) as ITerminalProxy;

View File

@ -1,20 +0,0 @@
// Allow to use "Authorization" from request for auto-login (when provided by proxy)
import { NextFunction, Request, Response } from "express"
import { userSession } from "../user-session";
export function useRequestHeaderToken() {
return (req: Request, res: Response, next: NextFunction) => {
const authorization = req.headers["authorization"] || req.headers["x-lens-kubectl-token"];
const { authHeader, isUserLogin } = userSession.get(req);
const userHasOwnToken = authHeader && isUserLogin;
// don't overwrite user's login credentials
if (authorization && !userHasOwnToken && authHeader !== authorization) {
userSession.save(req, {
authHeader: authorization.toString(),
});
}
next();
}
}

View File

@ -1,48 +0,0 @@
//-- 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;
}

View File

@ -1,5 +0,0 @@
export * from "./config-route"
export * from "./kubeconfig-route"
export * from "./kubewatch-route"
export * from "./metrics-route"
export * from "./ready-state-route"

View File

@ -1,100 +0,0 @@
//-- 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
}
}

View File

@ -1,139 +0,0 @@
//-- 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;
}

View File

@ -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;
}

View File

@ -1,16 +0,0 @@
//-- 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;
}

View File

@ -1,24 +0,0 @@
//-- User sessions helper
import { Request } from "express";
import CookieSessionObject = CookieSessionInterfaces.CookieSessionObject;
interface IUserSession extends CookieSessionObject {
authHeader: string;
username?: string;
isUserLogin?: boolean; // authorization via user's manual login with credentials
}
export const userSession = {
get(req: Request): Partial<IUserSession> {
return req.session;
},
save(req: Request, data: Partial<IUserSession> = {}) {
Object.assign(req.session, data);
},
getToken(req: Request): string {
const { authHeader = "" } = this.get(req);
const [type, token = ""] = authHeader.split(" ");
return token;
}
};

View File

@ -1,32 +0,0 @@
// Load & parse local kubernetes config (dev-only)
import * as jsYaml from "js-yaml"
import * as fs from "fs"
import * as os from "os"
import chalk from "chalk";
import { logger } from "./logger";
interface IKubeConfigParams {
clusterUrl: string;
userToken: string;
}
export function getKubeConfigDev(): Partial<IKubeConfigParams> {
const KUBE_CONFIG_FILE = process.env.KUBE_CONFIG_FILE;
if (!KUBE_CONFIG_FILE) {
return {}
}
let filePath = ""
try {
filePath = KUBE_CONFIG_FILE.replace("~", os.homedir());
const yaml = fs.readFileSync(filePath).toString();
const config = jsYaml.safeLoad(yaml);
return {
clusterUrl: config.clusters[0].cluster.server,
userToken: config.users[0].user.token,
}
} catch (err) {
logger.error(`[KUBE-CONFIG] Parsing config file ${chalk.bold(filePath)} failed.`, err)
return {};
}
}

View File

@ -1,36 +0,0 @@
import chalk from "chalk";
import * as ip from "ip"
const divider = chalk.gray('-----------------------------------');
export const logger = {
// Called when express.js app starts on given port w/o errors
appStarted: (port: string | number, title = 'Server started ') => {
console.log(chalk.underline.bold(title) + ` ${chalk.green('✓')}`);
console.log(`
${chalk.bold('Access URLs:')}
${divider}
Localhost: ${chalk.magenta(`http://localhost:${port}`)}
LAN: ${chalk.magenta(`http://${ip.address()}:${port}`)}
${divider}
`);
},
error(message: string, error: any) {
let errString = ""
try {
errString = JSON.stringify(error, null, 2);
} catch (e) {
errString = String(error);
}
console.error(chalk.bold.red(`[ERROR] -> ${message}`), errString);
}
};
export function sanitizeHeaders(headers: { [name: string]: string }) {
if (headers.Authorization) {
const [authType, authToken] = headers.Authorization.split(" ");
headers.Authorization = `${authType} *****`
}
return headers;
}

View File

@ -1,30 +0,0 @@
// Parse payload from jwt token
// Format: https://github.com/kontena/kube-oidc#openid-connect-and-kubernetes
import { base64 } from "../../client/utils/base64";
interface JwtPayload {
"azp": string;// "1077841816959-kkdh0lvq1au80qv4gtubotvgs9am4a95.apps.googleusercontent.com",
"aud": string;// "1077841816959-kkdh0lvq1au80qv4gtubotvgs9am4a95.apps.googleusercontent.com",
"sub": string;// "103613003764490648449",
"hd": string;// "redhat.com",
"email": string;// "echiang@redhat.com",
"email_verified": boolean; // true,
"at_hash": string;// "OGDOjIJ92FkatDBoCm8ydg",
"exp": number;// 1527203940,
"iss": string;// "https://accounts.google.com",
"iat": number;// 1527200340,
"name": string; // "Eric Chiang",
"picture": string; // "https://lh5.googleusercontent.com/-Cs2iHTXiETs/AAAAAAAAAAI/AAAAAAAAACM/0Q85UhZizjg/s96-c/photo.jpg",
"given_name": string; // "Eric",
"family_name": string; //"Chiang",
"locale": string; // "en"
}
export function parseJwt(token: string): Partial<JwtPayload> {
try {
const [header, payload, signature] = token.split(".");
return base64.decode(payload);
} catch (e) {
return {}
}
}

View File

@ -2631,11 +2631,6 @@ boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bowser@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.7.0.tgz#96eab1fa07fab08c1ec4c75977a7c8ddf8e0fe1f"
integrity sha512-aIlMvstvu8x+34KEiOHD3AsBgdrzg6sxALYiukOWhFvGMbQI6TRP/iY0LMhUrHs56aD6P1G0Z7h45PUJaa5m9w==
boxen@^1.2.1: boxen@^1.2.1:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@ -2919,11 +2914,6 @@ camelcase@^5.0.0, camelcase@^5.3.1:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
camelize@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
caniuse-lite@^1.0.30001030: caniuse-lite@^1.0.30001030:
version "1.0.30001033" version "1.0.30001033"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001033.tgz#60c328fb56860de60f9a2cb419c31fb80587cba0" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001033.tgz#60c328fb56860de60f9a2cb419c31fb80587cba0"
@ -3349,11 +3339,6 @@ content-disposition@0.5.3:
dependencies: dependencies:
safe-buffer "5.1.2" safe-buffer "5.1.2"
content-security-policy-builder@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz#0a2364d769a3d7014eec79ff7699804deb8cfcbb"
integrity sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==
content-type@~1.0.4: content-type@~1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@ -3637,11 +3622,6 @@ dashdash@^1.12.0:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
dasherize@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308"
integrity sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=
data-urls@^1.0.0: data-urls@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
@ -3788,11 +3768,6 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
depd@~1.1.2: depd@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@ -3870,11 +3845,6 @@ dns-packet@^1.3.1:
ip "^1.1.0" ip "^1.1.0"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
dns-prefetch-control@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz#73988161841f3dcc81f47686d539a2c702c88624"
integrity sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q==
dns-txt@^2.0.2: dns-txt@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
@ -3970,11 +3940,6 @@ domutils@^1.5.1:
dom-serializer "0" dom-serializer "0"
domelementtype "1" domelementtype "1"
dont-sniff-mimetype@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz#c7d0427f8bcb095762751252af59d148b0a623b2"
integrity sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==
dot-prop@^4.1.0: dot-prop@^4.1.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
@ -4354,11 +4319,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies: dependencies:
homedir-polyfill "^1.0.1" homedir-polyfill "^1.0.1"
expect-ct@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.2.0.tgz#3a54741b6ed34cc7a93305c605f63cd268a54a62"
integrity sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g==
expect@^24.9.0: expect@^24.9.0:
version "24.9.0" version "24.9.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
@ -4525,11 +4485,6 @@ fb-watchman@^2.0.0:
dependencies: dependencies:
bser "^2.0.0" bser "^2.0.0"
feature-policy@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069"
integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==
figgy-pudding@^3.5.1: figgy-pudding@^3.5.1:
version "3.5.1" version "3.5.1"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@ -4682,11 +4637,6 @@ fragment-cache@^0.2.1:
dependencies: dependencies:
map-cache "^0.2.2" map-cache "^0.2.2"
frameguard@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.1.0.tgz#bd1442cca1d67dc346a6751559b6d04502103a22"
integrity sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g==
fresh@0.5.2: fresh@0.5.2:
version "0.5.2" version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@ -5096,47 +5046,6 @@ he@1.2.x:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
helmet-crossdomain@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e"
integrity sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==
helmet-csp@2.9.4:
version "2.9.4"
resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.9.4.tgz#801382bac98f2f88706dc5c89d95c7e31af3a4a9"
integrity sha512-qUgGx8+yk7Xl8XFEGI4MFu1oNmulxhQVTlV8HP8tV3tpfslCs30OZz/9uQqsWPvDISiu/NwrrCowsZBhFADYqg==
dependencies:
bowser "^2.7.0"
camelize "1.0.0"
content-security-policy-builder "2.1.0"
dasherize "2.0.0"
helmet@^3.21.2:
version "3.21.2"
resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.21.2.tgz#7e2a19d5f6d898a77b5d2858e8e4bb2cda59f19f"
integrity sha512-okUo+MeWgg00cKB8Csblu8EXgcIoDyb5ZS/3u0W4spCimeVuCUvVZ6Vj3O2VJ1Sxpyb8jCDvzu0L1KKT11pkIg==
dependencies:
depd "2.0.0"
dns-prefetch-control "0.2.0"
dont-sniff-mimetype "1.1.0"
expect-ct "0.2.0"
feature-policy "0.3.0"
frameguard "3.1.0"
helmet-crossdomain "0.4.0"
helmet-csp "2.9.4"
hide-powered-by "1.1.0"
hpkp "2.0.0"
hsts "2.2.0"
ienoopen "1.1.0"
nocache "2.1.0"
referrer-policy "1.2.0"
x-xss-protection "1.3.0"
hide-powered-by@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a"
integrity sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==
history@^4.10.1, history@^4.9.0: history@^4.10.1, history@^4.9.0:
version "4.10.1" version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
@ -5187,18 +5096,6 @@ hpack.js@^2.1.6:
readable-stream "^2.0.1" readable-stream "^2.0.1"
wbuf "^1.1.0" wbuf "^1.1.0"
hpkp@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672"
integrity sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=
hsts@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964"
integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==
dependencies:
depd "2.0.0"
html-element-map@^1.0.0: html-element-map@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.1.0.tgz#e5aab9a834caf883b421f8bd9eaedcaac887d63c" resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.1.0.tgz#e5aab9a834caf883b421f8bd9eaedcaac887d63c"
@ -5308,16 +5205,6 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11" lodash "^4.17.11"
micromatch "^3.1.10" micromatch "^3.1.10"
http-proxy-middleware@^0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.20.0.tgz#5b128f7207985c4ea91b53fab8ad897a48c690d6"
integrity sha512-dNJAk71nEJhPiAczQH9hGvE/MT9kEs+zn2Dh+Hi94PGZe1GluQirC7mw5rdREUtWx6qGS1Gu0bZd4qEAg+REgw==
dependencies:
http-proxy "^1.17.0"
is-glob "^4.0.1"
lodash "^4.17.14"
micromatch "^4.0.2"
http-proxy@^1.17.0: http-proxy@^1.17.0:
version "1.18.0" version "1.18.0"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
@ -5372,11 +5259,6 @@ ieee754@^1.1.4:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
ienoopen@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974"
integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ==
iferr@^0.1.5: iferr@^0.1.5:
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
@ -7231,11 +7113,6 @@ no-case@^2.2.0:
dependencies: dependencies:
lower-case "^1.1.1" lower-case "^1.1.1"
nocache@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
node-forge@0.9.0: node-forge@0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
@ -8568,11 +8445,6 @@ redent@^1.0.0:
indent-string "^2.1.0" indent-string "^2.1.0"
strip-indent "^1.0.1" strip-indent "^1.0.1"
referrer-policy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e"
integrity sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==
reflect.ownkeys@^0.2.0: reflect.ownkeys@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
@ -10503,11 +10375,6 @@ ws@^6.2.1:
dependencies: dependencies:
async-limiter "~1.0.0" async-limiter "~1.0.0"
x-xss-protection@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.3.0.tgz#3e3a8dd638da80421b0e9fff11a2dbe168f6d52c"
integrity sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==
xdg-basedir@^3.0.0: xdg-basedir@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"

View File

@ -37,10 +37,6 @@
"AppImage" "AppImage"
], ],
"extraResources": [ "extraResources": [
{
"from": "binaries/server/linux/lens-server",
"to": "./lens-server.txt"
},
{ {
"from": "binaries/client/linux/x64/kubectl", "from": "binaries/client/linux/x64/kubectl",
"to": "./x64/kubectl" "to": "./x64/kubectl"
@ -53,10 +49,6 @@
"entitlements": "build/entitlements.mac.plist", "entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.plist",
"extraResources": [ "extraResources": [
{
"from": "binaries/server/darwin/lens-server",
"to": "./lens-server.txt"
},
{ {
"from": "binaries/client/darwin/x64/kubectl", "from": "binaries/client/darwin/x64/kubectl",
"to": "./x64/kubectl" "to": "./x64/kubectl"
@ -68,10 +60,6 @@
"nsis" "nsis"
], ],
"extraResources": [ "extraResources": [
{
"from": "binaries/server/win32/lens-server-x64.exe",
"to": "./lens-server-x64.exe"
},
{ {
"from": "binaries/server/win32/lens-server-ia32.exe", "from": "binaries/server/win32/lens-server-ia32.exe",
"to": "./lens-server-ia32.exe" "to": "./lens-server-ia32.exe"
@ -107,7 +95,7 @@
}, },
"scripts": { "scripts": {
"dev": "concurrently -n 'app,dash' 'yarn dev-electron' 'yarn dev-dashboard'", "dev": "concurrently -n 'app,dash' 'yarn dev-electron' 'yarn dev-dashboard'",
"dev-dashboard": "cd dashboard && yarn app-dev", "dev-dashboard": "cd dashboard && yarn dev",
"dev-electron": "electron-webpack dev", "dev-electron": "electron-webpack dev",
"compile": "yarn download:kubectl && electron-webpack", "compile": "yarn download:kubectl && electron-webpack",
"build:linux": "yarn compile && electron-builder --linux --dir", "build:linux": "yarn compile && electron-builder --linux --dir",
@ -168,6 +156,7 @@
"devDependencies": { "devDependencies": {
"@types/ejs": "^2.6.3", "@types/ejs": "^2.6.3",
"@types/electron-window-state": "^2.0.31", "@types/electron-window-state": "^2.0.31",
"@types/hapi": "^18.0.3",
"@types/http-proxy": "^1.17.0", "@types/http-proxy": "^1.17.0",
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.5", "@types/jsonwebtoken": "^8.3.5",

View File

@ -1,16 +1,12 @@
import { app } from "electron"
import { KubeConfig } from "@kubernetes/client-node" import { KubeConfig } from "@kubernetes/client-node"
import { readFileSync } from "fs" import { readFileSync } from "fs"
import * as http from "http" import * as http from "http"
import { ServerOptions } from "http-proxy" import { ServerOptions } from "http-proxy"
import * as url from "url" import * as url from "url"
import { v4 as uuid } from "uuid"
import logger from "./logger" import logger from "./logger"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { LensServer } from "./lens-server"
import { KubeAuthProxy } from "./kube-auth-proxy" import { KubeAuthProxy } from "./kube-auth-proxy"
import { Cluster, ClusterPreferences } from "./cluster" import { Cluster, ClusterPreferences } from "./cluster"
import { userStore } from "../common/user-store"
export class ContextHandler { export class ContextHandler {
public contextName: string public contextName: string
@ -24,14 +20,12 @@ export class ContextHandler {
protected apiTarget: ServerOptions protected apiTarget: ServerOptions
protected proxyTarget: ServerOptions protected proxyTarget: ServerOptions
protected clusterUrl: url.UrlWithStringQuery protected clusterUrl: url.UrlWithStringQuery
protected localServer: LensServer
protected proxyServer: KubeAuthProxy protected proxyServer: KubeAuthProxy
protected clientCert: string protected clientCert: string
protected clientKey: string protected clientKey: string
protected secureApiConnection = true protected secureApiConnection = true
protected defaultNamespace: string protected defaultNamespace: string
protected port: number
protected proxyPort: number protected proxyPort: number
protected kubernetesApi: string protected kubernetesApi: string
protected prometheusPath: string protected prometheusPath: string
@ -86,6 +80,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) {
@ -124,41 +122,6 @@ export class ContextHandler {
return this.apiTarget return this.apiTarget
} }
public async getProxyTarget() {
if (this.proxyTarget) {
return this.proxyTarget;
}
this.proxyTarget = {
changeOrigin: true,
secure: false,
target: {
host: this.clusterUrl.host,
hostname: "localhost",
path: "/",
port: await this.resolvePort(),
protocol: "http://",
},
}
return this.proxyTarget;
}
protected async resolvePort(): Promise<number> {
if (this.port) return this.port
let serverPort: number = null
try {
serverPort = await getFreePort(49153, 49900) // the proxy will usually already be on 49152 so skip that
} catch(error) {
logger.error(error)
throw(error)
}
this.port = serverPort
return serverPort
}
protected async resolveProxyPort(): Promise<number> { protected async resolveProxyPort(): Promise<number> {
if (this.proxyPort) return this.proxyPort if (this.proxyPort) return this.proxyPort
@ -186,35 +149,7 @@ export class ContextHandler {
} }
} }
protected initServer(serverUrl: string, port: number) {
const userPrefs = userStore.getPreferences()
const envs = {
KUBE_CLUSTER_URL: serverUrl,
KUBE_CLUSTER_NAME: this.clusterName,
KUBERNETES_TLS_SKIP: "true",
KUBERNETES_NAMESPACE: this.defaultNamespace,
SESSION_SECRET: this.id,
LOCAL_SERVER_PORT: port.toString(),
KUBE_METRICS_URL: `${serverUrl}/api/v1/namespaces/${this.prometheusPath}/proxy`,
STATS_NAMESPACE_DEFAULT: this.prometheusPath.split("/")[0],
CHARTS_ENABLED: "true",
LENS_VERSION: app.getVersion(),
LENS_THEME: `kontena-${userPrefs.colorTheme}`,
NODE_ENV: "production",
}
logger.debug(`spinning up lens-server process with env: ${JSON.stringify(envs)}`)
this.localServer = new LensServer(serverUrl, envs)
}
public async ensureServer() { public async ensureServer() {
if (!this.localServer) {
const currentCluster = this.kc.getCurrentCluster()
const clusterUrl = url.parse(currentCluster.server)
const serverPort = await this.resolvePort()
logger.info(`initializing server for ${clusterUrl.host} on port ${serverPort}`)
this.initServer(this.kubernetesApi, serverPort)
await this.localServer.run()
}
if (!this.proxyServer) { if (!this.proxyServer) {
const proxyPort = await this.resolveProxyPort() const proxyPort = await this.resolveProxyPort()
const proxyEnv = Object.assign({}, process.env) const proxyEnv = Object.assign({}, process.env)
@ -227,10 +162,6 @@ export class ContextHandler {
} }
public stopServer() { public stopServer() {
if (this.localServer) {
this.localServer.exit()
this.localServer = null
}
if (this.proxyServer) { if (this.proxyServer) {
this.proxyServer.exit() this.proxyServer.exit()
this.proxyServer = null this.proxyServer = null

View File

@ -1,63 +0,0 @@
import * as path from "path"
import { spawn, ChildProcess } from "child_process"
import logger from "./logger"
import * as tcpPortUsed from "tcp-port-used"
declare const __static: string;
const isDevelopment = process.env.NODE_ENV !== "production"
let serverPath: string = null
if (isDevelopment) {
serverPath = path.join(process.cwd(), "binaries", "server", process.platform, "lens-server")
} else {
serverPath = path.join(process.resourcesPath, "lens-server")
if (process.platform !== "win32") {
serverPath = `${serverPath}.txt`
}
}
if (process.platform === "win32") {
serverPath = `${serverPath}-${process.arch}.exe`
}
export class LensServer {
protected serverUrl: string = null
protected env: NodeJS.ProcessEnv = null
protected localServer: ChildProcess
constructor(serverUrl: string, env: NodeJS.ProcessEnv) {
this.serverUrl = serverUrl
this.env = env
}
public async run(): Promise<void> {
if (this.localServer) {
return new Promise((resolve, reject) => {
resolve()
})
}
this.localServer = spawn(serverPath, [], {
env: this.env,
cwd: __static
})
this.localServer.on("exit", (code) => {
logger.error(`server ${this.serverUrl} exited with code ${code}`)
this.localServer = null
})
this.localServer.stdout.on('data', (data) => {
logger.debug(`server ${this.serverUrl} stdout: ${data}`)
})
this.localServer.stderr.on('data', (data) => {
logger.debug(`server ${this.serverUrl} stderr: ${data}`)
})
return tcpPortUsed.waitUntilUsed(parseInt(this.env.LOCAL_SERVER_PORT), 500, 10000)
}
public exit() {
if (this.localServer) {
logger.debug(`Stopping local server: ${this.serverUrl}`)
this.localServer.kill()
this.localServer = null
}
}
}

View File

@ -35,11 +35,7 @@ export class LensProxy {
this.handleRequest(proxy, req, res); this.handleRequest(proxy, req, res);
}.bind(this)); }.bind(this));
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) { proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
if (this.isRemoteShellRequired(req)) { this.handleWsUpgrade(req, socket, head)
this.proxyWsUpgrade(proxy, req, socket, head)
} else {
this.handleWsUpgrade(req, socket, head)
}
}.bind(this)); }.bind(this));
proxyServer.on("error", (err) => { proxyServer.on("error", (err) => {
@ -135,8 +131,6 @@ export class LensProxy {
delete req.headers.authorization delete req.headers.authorization
req.url = req.url.replace("/api-kube", "") req.url = req.url.replace("/api-kube", "")
return await contextHandler.getApiTarget() return await contextHandler.getApiTarget()
} else {
return await contextHandler.getProxyTarget()
} }
} }
@ -158,24 +152,13 @@ export class LensProxy {
return return
} }
contextHandler.ensureServer().then(async () => { contextHandler.ensureServer().then(async () => {
if (await this.router.route(cluster, req, res)) return
const proxyTarget = await this.getProxyTarget(req, contextHandler) const proxyTarget = await this.getProxyTarget(req, contextHandler)
proxy.web(req, res, proxyTarget) if (proxyTarget) {
}) proxy.web(req, res, proxyTarget)
} } else {
await this.router.route(cluster, req, res)
protected async proxyWsUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: Socket, head: Buffer) {
const cluster = this.clusterManager.getClusterForRequest(req)
const contextHandler = cluster.contextHandler
contextHandler.applyHeaders(req);
const reqUrl = url.parse(req.url, true)
const urlParams = reqUrl.query
for (const [key, value] of Object.entries(urlParams)) {
if (key !== "token") {
req.headers["x-lens-param-" + key] = value
} }
} })
proxy.ws(req, socket, head, await contextHandler.getProxyTarget());
} }
protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) { protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) {
@ -187,13 +170,6 @@ export class LensProxy {
wsServer.emit("connection", con, req); wsServer.emit("connection", con, req);
}); });
} }
protected isRemoteShellRequired(req: http.IncomingMessage) {
if (!LensProxy.localShellSessions) {
return true
}
return false;
}
} }
export function listen(port: number, clusterManager: ClusterManager) { export function listen(port: number, clusterManager: ClusterManager) {

View File

@ -1,13 +1,23 @@
import * as http from "http"; import * as http from "http"
import { Cluster } from "./cluster"; import * as path from "path"
import { Cluster } from "./cluster"
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 { metricsRoute } from "./routes/metrics"
import { watchRoute } from "./routes/watch"
import { readFile } from "fs"
// 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');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Subtext = require('@hapi/subtext'); const Subtext = require('@hapi/subtext');
declare const __static: string;
const assetsPath = path.join(__static, "build/client")
interface RouteParams { interface RouteParams {
[key: string]: string | undefined; [key: string]: string | undefined;
} }
@ -64,7 +74,35 @@ export class Router {
return request return request
} }
protected handleStaticFile(file: string, response: http.ServerResponse) {
const asset = path.join(assetsPath, file)
readFile(asset, (err, data) => {
if (err) {
response.statusCode = 404
} else {
response.write(data)
response.end()
}
})
}
protected addRoutes() { protected addRoutes() {
// Static assets
this.router.add({ method: 'get', path: '/{path*}' }, (request: LensApiRequest) => {
const { response, params } = request
const file = params.path || "/index.html"
this.handleStaticFile(file, response)
})
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))
// Watch API
this.router.add({ method: 'get', path: '/api/watch' }, watchRoute.routeWatch.bind(watchRoute))
// 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))

66
src/main/routes/config.ts Normal file
View File

@ -0,0 +1,66 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { userStore } from "../../common/user-store"
import { getAppVersion } from "../../common/app-utils"
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
import { Cluster } from "../cluster"
function selfSubjectAccessReview(authApi: AuthorizationV1Api, namespace: string) {
return authApi.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: {
resourceAttributes: {
namespace: namespace,
resource: "pod",
verb: "list",
}
}
})
}
async function getAllowedNamespaces(cluster: Cluster) {
const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api)
const authApi = cluster.contextHandler.kc.makeApiClient(AuthorizationV1Api)
try {
const namespaceList = await api.listNamespace()
const nsAccessStatuses = await Promise.all(
namespaceList.body.items.map(ns => {
return selfSubjectAccessReview(authApi, ns.metadata.name)
})
)
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i].body.status.allowed)
.map(ns => ns.metadata.name)
} catch(error) {
const kc = cluster.contextHandler.kc
const ctx = kc.getContextObject(kc.currentContext)
if (ctx.namespace) {
return [ctx.namespace]
} else {
return []
}
}
}
class ConfigRoute extends LensApi {
public async routeConfig(request: LensApiRequest) {
const { params, response, cluster} = request
const data = {
clusterName: cluster.contextName,
lensVersion: getAppVersion(),
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
kubeVersion: cluster.version,
chartsEnabled: true,
isClusterAdmin: cluster.isAdmin,
allowedNamespaces: await getAllowedNamespaces(cluster)
};
this.respondJson(response, data)
}
}
export const configRoute = new ConfigRoute()

View File

@ -0,0 +1,58 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Cluster } from "../cluster"
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
const tokenData = new Buffer(secret.data["token"], "base64")
return {
'apiVersion': 'v1',
'kind': 'Config',
'clusters': [
{
'name': cluster.contextName,
'cluster': {
'server': cluster.contextHandler.kc.getCurrentCluster().server,
'certificate-authority-data': secret.data["ca.crt"]
}
}
],
'users': [
{
'name': username,
'user': {
'token': tokenData.toString("utf8"),
}
}
],
'contexts': [
{
'name': cluster.contextName,
'context': {
'user': username,
'cluster': cluster.contextName,
'namespace': secret.metadata.namespace,
}
}
],
'current-context': cluster.contextName
}
}
class KubeconfigRoute extends LensApi {
public async routeServiceAccountRoute(request: LensApiRequest) {
const { params, response, cluster} = request
const client = cluster.contextHandler.kc.makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace)
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata;
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
});
const data = generateKubeConfig(params.account, secret, cluster);
this.respondJson(response, data)
}
}
export const kubeconfigRoute = new KubeconfigRoute()

View 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()

108
src/main/routes/watch.ts Normal file
View File

@ -0,0 +1,108 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
import { ServerResponse } from "http"
import { Request } from "request"
import logger from "../logger"
class ApiWatcher {
private apiUrl: string
private response: ServerResponse
private watchRequest: Request
private watch: Watch
private processor: NodeJS.Timeout
private eventBuffer: any[] = []
constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
this.apiUrl = apiUrl
this.watch = new Watch(kubeConfig)
this.response = response
}
public start() {
if (this.processor) {
clearInterval(this.processor)
}
this.processor = setInterval(() => {
const events = this.eventBuffer.splice(0)
events.map(event => this.sendEvent(event))
this.response.flushHeaders()
}, 50)
this.watchRequest = this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this))
}
public stop() {
if (!this.watchRequest) { return }
if (this.processor) {
clearInterval(this.processor)
}
logger.debug("Stopping watcher for api: " + this.apiUrl)
this.watchRequest.abort()
}
private watchHandler(phase: string, obj: RuntimeRawExtension) {
this.eventBuffer.push({
type: phase,
object: obj
})
}
private doneHandler(error: Error) {
if (error) {
logger.error("watch error: " + error.toString())
this.sendEvent({
type: "STREAM_END",
url: this.apiUrl,
status: 410,
})
return
}
this.start()
}
private sendEvent(evt: any) {
// convert to "text/event-stream" format
this.response.write(`data: ${JSON.stringify(evt)}\n\n`);
}
}
class WatchRoute extends LensApi {
public async routeWatch(request: LensApiRequest) {
const { params, response, cluster} = request
const apis: string[] = request.query.getAll("api")
const watchers: ApiWatcher[] = []
if (!apis.length) {
this.respondJson(response, {
message: "Empty request. Query params 'api' are not provided.",
example: "?api=/api/v1/pods&api=/api/v1/nodes",
}, 400)
return
}
response.setHeader("Content-Type", "text/event-stream")
response.setHeader("Cache-Control", "no-cache")
response.setHeader("Connection", "keep-alive")
apis.forEach(apiUrl => {
const watcher = new ApiWatcher(apiUrl, cluster.contextHandler.kc, response)
watcher.start()
watchers.push(watcher)
})
request.raw.req.on("close", () => {
logger.debug("Watch request closed")
watchers.map(watcher => watcher.stop())
})
request.raw.req.on("end", () => {
logger.debug("Watch request ended")
watchers.map(watcher => watcher.stop())
})
}
}
export const watchRoute = new WatchRoute()

View File

@ -1104,11 +1104,21 @@
dependencies: dependencies:
"@babel/types" "^7.3.0" "@babel/types" "^7.3.0"
"@types/boom@*":
version "7.3.0"
resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.3.0.tgz#33280c5552d4cfabc21b8b7e0f6d29292decd985"
integrity sha512-PH7bfkt1nu4pnlxz+Ws+wwJJF1HE12W3ia+Iace2JT7q56DLH3hbyjOJyNHJYRxk3PkKaC36fHfHKyeG1rMgCA==
"@types/caseless@*": "@types/caseless@*":
version "0.12.2" version "0.12.2"
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
"@types/catbox@*":
version "10.0.6"
resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-10.0.6.tgz#8a4c91261cf0afca03351bb82a95b2d6cf43a5d0"
integrity sha512-qS0VHlL6eBUUoUeBnI/ASCffoniS62zdV6IUtLSIjGKmRhZNawotaOMsTYivZOTZVktfe9koAJkD9XFac7tEEg==
"@types/color-name@^1.1.1": "@types/color-name@^1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@ -1176,6 +1186,20 @@
"@types/minimatch" "*" "@types/minimatch" "*"
"@types/node" "*" "@types/node" "*"
"@types/hapi@^18.0.3":
version "18.0.3"
resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-18.0.3.tgz#e74c019f6a1b1c7f647fe014d3890adec9c0214a"
integrity sha512-UM03myDZ2UWbpqLSZqboK4L98F9r4GCcd9JOr2auhgC3iOd/69mvDggivOHhOYJKWHeCW/dECPHIB7DwQz00dw==
dependencies:
"@types/boom" "*"
"@types/catbox" "*"
"@types/iron" "*"
"@types/joi" "*"
"@types/mimos" "*"
"@types/node" "*"
"@types/podium" "*"
"@types/shot" "*"
"@types/http-proxy@^1.17.0": "@types/http-proxy@^1.17.0":
version "1.17.0" version "1.17.0"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.0.tgz#baf82ff6aa2723fd29f90e3ba1384e665006863e" resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.0.tgz#baf82ff6aa2723fd29f90e3ba1384e665006863e"
@ -1183,6 +1207,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/iron@*":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/iron/-/iron-5.0.1.tgz#5420bbda8623c48ee51b9a78ebad05d7305b4b24"
integrity sha512-Ng5BkVGPt7Tw9k1OJ6qYwuD9+dmnWgActmsnnrdvs4075N8V2go1f6Pz8omG3q5rbHjXN6yzzZDYo3JOgAE/Ug==
dependencies:
"@types/node" "*"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@ -1215,6 +1246,11 @@
dependencies: dependencies:
"@types/jest-diff" "*" "@types/jest-diff" "*"
"@types/joi@*":
version "14.3.4"
resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0"
integrity sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A==
"@types/js-yaml@^3.12.1": "@types/js-yaml@^3.12.1":
version "3.12.1" version "3.12.1"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656"
@ -1237,6 +1273,18 @@
resolved "https://registry.yarnpkg.com/@types/md5-file/-/md5-file-4.0.0.tgz#01c986144648a757ac6e7ada665fa665c253289d" resolved "https://registry.yarnpkg.com/@types/md5-file/-/md5-file-4.0.0.tgz#01c986144648a757ac6e7ada665fa665c253289d"
integrity sha512-t+qg7R25oYo6z3iWI+9CRky2mgQ51RGvLqlCPV+xa6dKp0YDomv0TArLK/CdcReFZwQHn/YMNRZx+4AUWXPtlg== integrity sha512-t+qg7R25oYo6z3iWI+9CRky2mgQ51RGvLqlCPV+xa6dKp0YDomv0TArLK/CdcReFZwQHn/YMNRZx+4AUWXPtlg==
"@types/mime-db@*":
version "1.27.0"
resolved "https://registry.yarnpkg.com/@types/mime-db/-/mime-db-1.27.0.tgz#9bc014a1fd1fdf47649c1a54c6dd7966b8284792"
integrity sha1-m8AUof0f30dknBpUxt15ZrgoR5I=
"@types/mimos@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mimos/-/mimos-3.0.1.tgz#59d96abe1c9e487e7463fe41e8d86d76b57a441a"
integrity sha512-MATIRH4VMIJki8lcYUZdNQEHuAG7iQ1FWwoLgxV+4fUOly2xZYdhHtGgvQyWiTeJqq2tZbE0nOOgZD6pR0FpNQ==
dependencies:
"@types/mime-db" "*"
"@types/minimatch@*": "@types/minimatch@*":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -1276,6 +1324,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44"
integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg== integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==
"@types/podium@*":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/podium/-/podium-1.0.0.tgz#bfaa2151be2b1d6109cc69f7faa9dac2cba3bb20"
integrity sha1-v6ohUb4rHWEJzGn3+qnawsujuyA=
"@types/proper-lockfile@^4.1.1": "@types/proper-lockfile@^4.1.1":
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz#99f026cbfdbe6305bdd454ffd5fefc1bd064beb1" resolved "https://registry.yarnpkg.com/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz#99f026cbfdbe6305bdd454ffd5fefc1bd064beb1"
@ -1338,6 +1391,13 @@
"@types/glob" "*" "@types/glob" "*"
"@types/node" "*" "@types/node" "*"
"@types/shot@*":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/shot/-/shot-4.0.0.tgz#7545500c489b65c69b5bc5446ba4fef3bd26af92"
integrity sha512-Xv+n8yfccuicMlwBY58K5PVVNtXRm7uDzcwwmCarBxMP+XxGfnh1BI06YiVAsPbTAzcnYsrzpoS5QHeyV7LS8A==
dependencies:
"@types/node" "*"
"@types/stack-utils@^1.0.1": "@types/stack-utils@^1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"