diff --git a/Makefile b/Makefile
index 10184e370f..4a8348c419 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ endif
.PHONY: dev build test clean
-dev: app-deps dashboard-deps build-dashboard-server
+dev: app-deps dashboard-deps
yarn dev
test: test-app test-dashboard
@@ -39,25 +39,8 @@ clean-dashboard:
test-dashboard: dashboard-deps
cd dashboard && yarn test
-build-dashboard: build-dashboard-server build-dashboard-client
-
-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
+build-dashboard: dashboard-deps clean-dashboard
+ cd dashboard && yarn build
clean:
rm -rf dist/*
diff --git a/dashboard/client/components/app.tsx b/dashboard/client/components/app.tsx
index 882bcdfcab..b07d72749c 100755
--- a/dashboard/client/components/app.tsx
+++ b/dashboard/client/components/app.tsx
@@ -14,6 +14,7 @@ import { UserManagement } from "./+user-management/user-management";
import { ConfirmDialog } from "./confirm-dialog";
import { usersManagementRoute } from "./+user-management/user-management.routes";
import { clusterRoute, clusterURL } from "./+cluster";
+import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
import { Nodes, nodesRoute } from "./+nodes";
import { Workloads, workloadsRoute, workloadsURL } from "./+workloads";
import { Namespaces, namespacesRoute } from "./+namespaces";
@@ -70,6 +71,7 @@ class App extends React.Component {
+
diff --git a/dashboard/package.json b/dashboard/package.json
index 1fa5b05984..3cc7863c34 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -1,31 +1,15 @@
{
"name": "kontena-lens",
- "version": "1.13.1",
+ "version": "0.0.0",
"scripts": {
- "app-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-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",
+ "dev": "yarn extract && webpack-cli --watch --cache --progress --output-path ../static/build/client/",
+ "build": "yarn extract && NODE_ENV=production webpack -p --cache --progress --output-path ../static/build/client/",
"test": "jest --config './test/jest.config.js'",
"add-locale": "lingui add-locale",
"extract": "lingui extract --clean",
"compile": "lingui compile"
},
- "nodemonConfig": {
- "watch": [
- ".env",
- "package.json",
- "server"
- ],
- "ext": "ts",
- "execMap": {
- "ts": "ts-node --project server/tsconfig.json"
- }
- },
"dependencies": {
- "@types/jsonpath": "^0.2.0",
"axios": "^0.19.0",
"chalk": "^2.4.2",
"compression": "^1.7.4",
@@ -33,9 +17,6 @@
"cors": "^2.8.5",
"crypto-js": "^3.1.9-1",
"dotenv": "^8.2.0",
- "express": "^4.17.1",
- "helmet": "^3.21.2",
- "http-proxy-middleware": "^0.20.0",
"ip": "^1.1.5",
"js-yaml": "^3.13.1",
"jsonpath": "^1.0.2",
@@ -76,6 +57,7 @@
"@types/http-proxy-middleware": "^0.19.3",
"@types/ip": "^1.1.0",
"@types/jest": "^24.0.22",
+ "@types/jsonpath": "^0.2.0",
"@types/js-yaml": "^3.12.1",
"@types/lingui__macro": "^2.7.3",
"@types/lodash": "^4.14.146",
diff --git a/dashboard/server/api/get-cert-auth-data.ts b/dashboard/server/api/get-cert-auth-data.ts
deleted file mode 100644
index 8c32b3ce17..0000000000
--- a/dashboard/server/api/get-cert-auth-data.ts
+++ /dev/null
@@ -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 {
- 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 ''
- }
-}
diff --git a/dashboard/server/api/get-cluster-info.ts b/dashboard/server/api/get-cluster-info.ts
deleted file mode 100644
index c24178741a..0000000000
--- a/dashboard/server/api/get-cluster-info.ts
+++ /dev/null
@@ -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 {
- 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;
-}
diff --git a/dashboard/server/api/get-namespaces.ts b/dashboard/server/api/get-namespaces.ts
deleted file mode 100644
index 4aad15a6b3..0000000000
--- a/dashboard/server/api/get-namespaces.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// Get namespaces
-
-import config from "../config";
-import { KubeJsonApiDataList } from "../../client/api/kube-json-api";
-import { IKubeRequestParams, kubeRequest } from "./kube-request";
-import { reviewResourceAccess } from "./review-resource-access";
-import { getServiceAccountToken } from "./get-service-account-token"
-
-export async function getNamespaces(params: Partial = {}) {
- return kubeRequest({
- ...params,
- path: "/api/v1/namespaces",
- });
-}
-
-export async function getAllowedNamespaces(
- params: Partial,
- fallbackNs = config.KUBERNETES_NAMESPACE,
-): Promise {
- try {
- const allNamespaces = await getNamespaces(params);
- const nsAccessStatuses = await Promise.all(
- allNamespaces.items.map(ns => {
- const { name } = ns.metadata;
- return reviewResourceAccess(params, {
- namespace: name,
- resource: "pods",
- verb: "list",
- });
- })
- );
- return allNamespaces.items
- .filter((ns, i) => nsAccessStatuses[i].allowed)
- .map(ns => ns.metadata.name);
- } catch (e) {
- const serviceToken = await getServiceAccountToken();
- if (!serviceToken) {
- return fallbackNs ? [fallbackNs] : [];
- }
- // fetch namespaces with service-account token (cluster-wide)
- // and for every namespace make additional request to check if namespace available for user-token
- const allNamespaces = await getNamespaces({
- authHeader: `Bearer ${serviceToken}`
- });
- const nsAccessStatuses = await Promise.all(
- allNamespaces.items.map(ns => {
- const { name } = ns.metadata;
- return reviewResourceAccess(params, {
- namespace: name,
- resource: "pods",
- verb: "list",
- });
- })
- );
- return allNamespaces.items
- .filter((ns, i) => nsAccessStatuses[i].allowed)
- .map(ns => ns.metadata.name);
- }
-}
diff --git a/dashboard/server/api/get-service-account-token.ts b/dashboard/server/api/get-service-account-token.ts
deleted file mode 100644
index 0728c26ab7..0000000000
--- a/dashboard/server/api/get-service-account-token.ts
+++ /dev/null
@@ -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;
-}
diff --git a/dashboard/server/api/is-cluster-admin.ts b/dashboard/server/api/is-cluster-admin.ts
deleted file mode 100644
index 90c7d1b94e..0000000000
--- a/dashboard/server/api/is-cluster-admin.ts
+++ /dev/null
@@ -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): Promise {
- try {
- const accessCheck = await reviewResourceAccess(params, {
- resource: "*",
- namespace: "*",
- group: "*",
- verb: "*",
- });
- return accessCheck.allowed;
- } catch (err) {
- return false;
- }
-}
diff --git a/dashboard/server/api/kube-request.ts b/dashboard/server/api/kube-request.ts
deleted file mode 100644
index c62cef4a36..0000000000
--- a/dashboard/server/api/kube-request.ts
+++ /dev/null
@@ -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(params: IKubeRequestParams): Promise {
- 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) => {
- 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;
- });
-}
diff --git a/dashboard/server/api/review-resource-access.ts b/dashboard/server/api/review-resource-access.ts
deleted file mode 100644
index 6ec4e5536a..0000000000
--- a/dashboard/server/api/review-resource-access.ts
+++ /dev/null
@@ -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 = {},
- attrs: IResourceAccessAttributes
-): Promise {
- try {
- const accessReview = await kubeRequest({
- ...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(),
- }
- }
-}
diff --git a/dashboard/server/api/review-token.ts b/dashboard/server/api/review-token.ts
deleted file mode 100644
index eb132e9acd..0000000000
--- a/dashboard/server/api/review-token.ts
+++ /dev/null
@@ -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 {
- try {
- const tokenReview = await kubeRequest({
- 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()],
- }
- }
-}
diff --git a/dashboard/server/app.ts b/dashboard/server/app.ts
deleted file mode 100644
index 73da918e89..0000000000
--- a/dashboard/server/app.ts
+++ /dev/null
@@ -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');
-});
diff --git a/dashboard/server/common/cluster.ts b/dashboard/server/common/cluster.ts
index 6ab519df14..fad6991626 100644
--- a/dashboard/server/common/cluster.ts
+++ b/dashboard/server/common/cluster.ts
@@ -1,9 +1,4 @@
-export interface IClusterConfigMap {
- clusterName: string;
- clusterUrl: string;
-}
-
-export interface IClusterInfo extends IClusterConfigMap {
+export interface IClusterInfo {
kubeVersion?: string;
- pharosVersion?: string;
+ clusterName?: string;
}
diff --git a/dashboard/server/middlewares/index.ts b/dashboard/server/middlewares/index.ts
deleted file mode 100644
index a0204435e8..0000000000
--- a/dashboard/server/middlewares/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from "./kube-proxy"
-export * from "./terminal-proxy"
-export * from "./use-header-token"
diff --git a/dashboard/server/middlewares/kube-proxy.ts b/dashboard/server/middlewares/kube-proxy.ts
deleted file mode 100644
index 5313566a00..0000000000
--- a/dashboard/server/middlewares/kube-proxy.ts
+++ /dev/null
@@ -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,
- })
-}
diff --git a/dashboard/server/middlewares/terminal-proxy.ts b/dashboard/server/middlewares/terminal-proxy.ts
deleted file mode 100644
index a710cb2320..0000000000
--- a/dashboard/server/middlewares/terminal-proxy.ts
+++ /dev/null
@@ -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;
\ No newline at end of file
diff --git a/dashboard/server/middlewares/use-header-token.ts b/dashboard/server/middlewares/use-header-token.ts
deleted file mode 100644
index 83f6893503..0000000000
--- a/dashboard/server/middlewares/use-header-token.ts
+++ /dev/null
@@ -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();
- }
-}
diff --git a/dashboard/server/routes/config-route.ts b/dashboard/server/routes/config-route.ts
deleted file mode 100644
index 25b8dccbc6..0000000000
--- a/dashboard/server/routes/config-route.ts
+++ /dev/null
@@ -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[] = [
- 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;
-}
diff --git a/dashboard/server/routes/index.ts b/dashboard/server/routes/index.ts
deleted file mode 100644
index d7ca1a4cb1..0000000000
--- a/dashboard/server/routes/index.ts
+++ /dev/null
@@ -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"
diff --git a/dashboard/server/routes/kubeconfig-route.ts b/dashboard/server/routes/kubeconfig-route.ts
deleted file mode 100644
index 9ed0f445e6..0000000000
--- a/dashboard/server/routes/kubeconfig-route.ts
+++ /dev/null
@@ -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
- }
-}
diff --git a/dashboard/server/routes/kubewatch-route.ts b/dashboard/server/routes/kubewatch-route.ts
deleted file mode 100644
index cd85fd0222..0000000000
--- a/dashboard/server/routes/kubewatch-route.ts
+++ /dev/null
@@ -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();
- const eventsBuffer = new Map();
- 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({
- 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;
-}
diff --git a/dashboard/server/routes/metrics-route.ts b/dashboard/server/routes/metrics-route.ts
deleted file mode 100644
index 8234432098..0000000000
--- a/dashboard/server/routes/metrics-route.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-//-- Metrics
-// https://prometheus.io/docs/prometheus/latest/querying/api/
-
-import { Router } from "express";
-import config from "../config";
-import { kubeRequest } from "../api/kube-request";
-import { userSession } from "../user-session";
-import { AxiosError } from "axios";
-import { IMetrics } from "../../client/api/endpoints/metrics.api";
-import { IMetricsQuery } from "../common/metrics"
-
-
-export function metricsRoute() {
- const router = Router();
-
- router.post("/metrics", async (req, res, next) => {
- const { authHeader } = userSession.get(req);
- const { namespace, ...queryParams } = req.query;
- const query: IMetricsQuery = req.body;
-
- /*eslint-disable */
- // add default namespace for rbac-proxy validation
- if (!queryParams.kubernetes_namespace) {
- queryParams.kubernetes_namespace = config.STATS_NAMESPACE;
- }
- /*eslint-enble */
-
- // prometheus metrics loader
- const attempts: { [query: string]: number } = {};
- const maxAttempts = 5;
- const loadMetrics = (query: string): Promise => {
- const attempt = attempts[query] = (attempts[query] || 0) + 1;
- return kubeRequest({
- url: config.KUBE_METRICS_URL,
- path: "/api/v1/query_range",
- authHeader: authHeader,
- params: {
- query: query,
- ...queryParams,
- },
- }).catch(async (err: AxiosError) => {
- // https://github.com/axios/axios#handling-errors
- if (!err.response && attempt < maxAttempts) {
- await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
- return loadMetrics(query);
- }
- return {
- status: err.toString(),
- data: {
- result: []
- },
- } as IMetrics;
- })
- };
-
- // return data in same structure as query
- let data: any;
- try {
- if (typeof query === "string") {
- data = await loadMetrics(query)
- }
- else if (Array.isArray(query)) {
- data = await Promise.all(query.map(loadMetrics));
- }
- else {
- data = {};
- const result = await Promise.all(
- Object.values(query).map(loadMetrics)
- );
- Object.keys(query).forEach((metricName, index) => {
- data[metricName] = result[index];
- });
- }
-
- res.json(data);
- } catch (err) {
- next(err);
- }
- });
-
- return router;
-}
diff --git a/dashboard/server/routes/ready-state-route.ts b/dashboard/server/routes/ready-state-route.ts
deleted file mode 100644
index 93b4ba50e1..0000000000
--- a/dashboard/server/routes/ready-state-route.ts
+++ /dev/null
@@ -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;
-}
diff --git a/dashboard/server/user-session.ts b/dashboard/server/user-session.ts
deleted file mode 100644
index b4e989adb5..0000000000
--- a/dashboard/server/user-session.ts
+++ /dev/null
@@ -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 {
- return req.session;
- },
- save(req: Request, data: Partial = {}) {
- Object.assign(req.session, data);
- },
- getToken(req: Request): string {
- const { authHeader = "" } = this.get(req);
- const [type, token = ""] = authHeader.split(" ");
- return token;
- }
-};
diff --git a/dashboard/server/utils/kube-config.dev.ts b/dashboard/server/utils/kube-config.dev.ts
deleted file mode 100644
index a3543f8124..0000000000
--- a/dashboard/server/utils/kube-config.dev.ts
+++ /dev/null
@@ -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 {
- 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 {};
- }
-}
diff --git a/dashboard/server/utils/logger.ts b/dashboard/server/utils/logger.ts
deleted file mode 100644
index d7fbc1fc3d..0000000000
--- a/dashboard/server/utils/logger.ts
+++ /dev/null
@@ -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;
-}
diff --git a/dashboard/server/utils/parse-jwt.ts b/dashboard/server/utils/parse-jwt.ts
deleted file mode 100644
index f840d65db7..0000000000
--- a/dashboard/server/utils/parse-jwt.ts
+++ /dev/null
@@ -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 {
- try {
- const [header, payload, signature] = token.split(".");
- return base64.decode(payload);
- } catch (e) {
- return {}
- }
-}
diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock
index 28b14a1d52..09e219d83e 100644
--- a/dashboard/yarn.lock
+++ b/dashboard/yarn.lock
@@ -2631,11 +2631,6 @@ boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
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:
version "1.3.0"
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"
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:
version "1.0.30001033"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001033.tgz#60c328fb56860de60f9a2cb419c31fb80587cba0"
@@ -3349,11 +3339,6 @@ content-disposition@0.5.3:
dependencies:
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:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@@ -3637,11 +3622,6 @@ dashdash@^1.12.0:
dependencies:
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:
version "1.1.0"
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"
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:
version "1.1.2"
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"
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:
version "2.0.2"
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"
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:
version "4.2.0"
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:
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:
version "24.9.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
@@ -4525,11 +4485,6 @@ fb-watchman@^2.0.0:
dependencies:
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:
version "3.5.1"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -4682,11 +4637,6 @@ fragment-cache@^0.2.1:
dependencies:
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:
version "0.5.2"
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"
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:
version "4.10.1"
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"
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:
version "1.1.0"
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"
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:
version "1.18.0"
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"
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:
version "0.1.5"
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
@@ -7231,11 +7113,6 @@ no-case@^2.2.0:
dependencies:
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:
version "0.9.0"
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"
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:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
@@ -10503,11 +10375,6 @@ ws@^6.2.1:
dependencies:
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:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
diff --git a/package.json b/package.json
index 9ea87de538..f2d84b039f 100644
--- a/package.json
+++ b/package.json
@@ -37,10 +37,6 @@
"AppImage"
],
"extraResources": [
- {
- "from": "binaries/server/linux/lens-server",
- "to": "./lens-server.txt"
- },
{
"from": "binaries/client/linux/x64/kubectl",
"to": "./x64/kubectl"
@@ -53,10 +49,6 @@
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"extraResources": [
- {
- "from": "binaries/server/darwin/lens-server",
- "to": "./lens-server.txt"
- },
{
"from": "binaries/client/darwin/x64/kubectl",
"to": "./x64/kubectl"
@@ -68,10 +60,6 @@
"nsis"
],
"extraResources": [
- {
- "from": "binaries/server/win32/lens-server-x64.exe",
- "to": "./lens-server-x64.exe"
- },
{
"from": "binaries/server/win32/lens-server-ia32.exe",
"to": "./lens-server-ia32.exe"
@@ -107,7 +95,7 @@
},
"scripts": {
"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",
"compile": "yarn download:kubectl && electron-webpack",
"build:linux": "yarn compile && electron-builder --linux --dir",
@@ -168,6 +156,7 @@
"devDependencies": {
"@types/ejs": "^2.6.3",
"@types/electron-window-state": "^2.0.31",
+ "@types/hapi": "^18.0.3",
"@types/http-proxy": "^1.17.0",
"@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.5",
diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts
index 80e14474c6..3abd97ea26 100644
--- a/src/main/context-handler.ts
+++ b/src/main/context-handler.ts
@@ -1,16 +1,12 @@
-import { app } from "electron"
import { KubeConfig } from "@kubernetes/client-node"
import { readFileSync } from "fs"
import * as http from "http"
import { ServerOptions } from "http-proxy"
import * as url from "url"
-import { v4 as uuid } from "uuid"
import logger from "./logger"
import { getFreePort } from "./port"
-import { LensServer } from "./lens-server"
import { KubeAuthProxy } from "./kube-auth-proxy"
import { Cluster, ClusterPreferences } from "./cluster"
-import { userStore } from "../common/user-store"
export class ContextHandler {
public contextName: string
@@ -24,14 +20,12 @@ export class ContextHandler {
protected apiTarget: ServerOptions
protected proxyTarget: ServerOptions
protected clusterUrl: url.UrlWithStringQuery
- protected localServer: LensServer
protected proxyServer: KubeAuthProxy
protected clientCert: string
protected clientKey: string
protected secureApiConnection = true
protected defaultNamespace: string
- protected port: number
protected proxyPort: number
protected kubernetesApi: string
protected prometheusPath: string
@@ -86,6 +80,10 @@ export class ContextHandler {
}
}
+ public getPrometheusPath() {
+ return this.prometheusPath
+ }
+
public async init() {
const currentCluster = this.kc.getCurrentCluster()
if (currentCluster.caFile) {
@@ -124,41 +122,6 @@ export class ContextHandler {
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 {
- 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 {
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() {
- 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) {
const proxyPort = await this.resolveProxyPort()
const proxyEnv = Object.assign({}, process.env)
@@ -227,10 +162,6 @@ export class ContextHandler {
}
public stopServer() {
- if (this.localServer) {
- this.localServer.exit()
- this.localServer = null
- }
if (this.proxyServer) {
this.proxyServer.exit()
this.proxyServer = null
diff --git a/src/main/lens-server.ts b/src/main/lens-server.ts
deleted file mode 100644
index 83192d7f4f..0000000000
--- a/src/main/lens-server.ts
+++ /dev/null
@@ -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 {
- 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
- }
- }
-}
diff --git a/src/main/proxy.ts b/src/main/proxy.ts
index 6600b1467e..55be9321f5 100644
--- a/src/main/proxy.ts
+++ b/src/main/proxy.ts
@@ -35,11 +35,7 @@ export class LensProxy {
this.handleRequest(proxy, req, res);
}.bind(this));
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
- if (this.isRemoteShellRequired(req)) {
- this.proxyWsUpgrade(proxy, req, socket, head)
- } else {
- this.handleWsUpgrade(req, socket, head)
- }
+ this.handleWsUpgrade(req, socket, head)
}.bind(this));
proxyServer.on("error", (err) => {
@@ -135,8 +131,6 @@ export class LensProxy {
delete req.headers.authorization
req.url = req.url.replace("/api-kube", "")
return await contextHandler.getApiTarget()
- } else {
- return await contextHandler.getProxyTarget()
}
}
@@ -158,24 +152,13 @@ export class LensProxy {
return
}
contextHandler.ensureServer().then(async () => {
- if (await this.router.route(cluster, req, res)) return
const proxyTarget = await this.getProxyTarget(req, contextHandler)
- proxy.web(req, res, proxyTarget)
- })
- }
-
- 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
+ if (proxyTarget) {
+ proxy.web(req, res, proxyTarget)
+ } else {
+ await this.router.route(cluster, req, res)
}
- }
- proxy.ws(req, socket, head, await contextHandler.getProxyTarget());
+ })
}
protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) {
@@ -187,13 +170,6 @@ export class LensProxy {
wsServer.emit("connection", con, req);
});
}
-
- protected isRemoteShellRequired(req: http.IncomingMessage) {
- if (!LensProxy.localShellSessions) {
- return true
- }
- return false;
- }
}
export function listen(port: number, clusterManager: ClusterManager) {
diff --git a/src/main/router.ts b/src/main/router.ts
index 04abad4c7c..bdd51c57f0 100644
--- a/src/main/router.ts
+++ b/src/main/router.ts
@@ -1,13 +1,23 @@
-import * as http from "http";
-import { Cluster } from "./cluster";
+import * as http from "http"
+import * as path from "path"
+import { Cluster } from "./cluster"
+import { configRoute } from "./routes/config"
import { helmApi } from "./helm-api"
import { resourceApplierApi } from "./resource-applier-api"
+import { kubeconfigRoute } from "./routes/kubeconfig"
+import { metricsRoute } from "./routes/metrics"
+import { watchRoute } from "./routes/watch"
+import { readFile } from "fs"
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Call = require('@hapi/call');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Subtext = require('@hapi/subtext');
+declare const __static: string;
+
+const assetsPath = path.join(__static, "build/client")
+
interface RouteParams {
[key: string]: string | undefined;
}
@@ -64,7 +74,35 @@ export class Router {
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() {
+ // 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
this.router.add({ method: 'get', path: '/api-helm/v2/charts' }, helmApi.listCharts.bind(helmApi))
this.router.add({ method: 'get', path: '/api-helm/v2/charts/{repo}/{chart}' }, helmApi.getChart.bind(helmApi))
diff --git a/src/main/routes/config.ts b/src/main/routes/config.ts
new file mode 100644
index 0000000000..28b328c1a7
--- /dev/null
+++ b/src/main/routes/config.ts
@@ -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()
diff --git a/src/main/routes/kubeconfig.ts b/src/main/routes/kubeconfig.ts
new file mode 100644
index 0000000000..6a6f173f3a
--- /dev/null
+++ b/src/main/routes/kubeconfig.ts
@@ -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()
diff --git a/src/main/routes/metrics.ts b/src/main/routes/metrics.ts
new file mode 100644
index 0000000000..51940956c6
--- /dev/null
+++ b/src/main/routes/metrics.ts
@@ -0,0 +1,75 @@
+import { LensApiRequest } from "../router"
+import { LensApi } from "../lens-api"
+import * as requestPromise from "request-promise-native"
+
+type MetricsQuery = string | string[] | {
+ [metricName: string]: string;
+}
+
+class MetricsRoute extends LensApi {
+
+ public async routeMetrics(request: LensApiRequest) {
+ const { response, cluster} = request
+ const query: MetricsQuery = request.payload;
+ const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
+ const metricsUrl = `${serverUrl}/api/v1/namespaces/${cluster.contextHandler.getPrometheusPath()}/proxy/api/v1/query_range`
+ const headers = {
+ "Host": `${cluster.id}.localhost:${cluster.port}`,
+ "Content-type": "application/json",
+ }
+ const queryParams: MetricsQuery = {}
+ request.query.forEach((value: string, key: string) => {
+ queryParams[key] = value
+ })
+
+ // prometheus metrics loader
+ const attempts: { [query: string]: number } = {};
+ const maxAttempts = 5;
+ const loadMetrics = (orgQuery: string): Promise => {
+ const query = orgQuery.trim()
+ const attempt = attempts[query] = (attempts[query] || 0) + 1;
+ return requestPromise(metricsUrl, {
+ resolveWithFullResponse: false,
+ headers: headers,
+ json: true,
+ qs: {
+ query: query,
+ ...queryParams
+ }
+ }).catch(async (error) => {
+ if (attempt < maxAttempts) {
+ await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
+ return loadMetrics(query);
+ }
+ return {
+ status: error.toString(),
+ data: {
+ result: []
+ }
+ }
+ })
+ };
+
+ // return data in same structure as query
+ let data: any;
+ if (typeof query === "string") {
+ data = await loadMetrics(query)
+ }
+ else if (Array.isArray(query)) {
+ data = await Promise.all(query.map(loadMetrics));
+ }
+ else {
+ data = {};
+ const result = await Promise.all(
+ Object.values(query).map(loadMetrics)
+ );
+ Object.keys(query).forEach((metricName, index) => {
+ data[metricName] = result[index];
+ });
+ }
+
+ this.respondJson(response, data)
+ }
+}
+
+export const metricsRoute = new MetricsRoute()
diff --git a/src/main/routes/watch.ts b/src/main/routes/watch.ts
new file mode 100644
index 0000000000..077d70ce0f
--- /dev/null
+++ b/src/main/routes/watch.ts
@@ -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()
diff --git a/yarn.lock b/yarn.lock
index 6ebcbe543d..86b3a08da4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1104,11 +1104,21 @@
dependencies:
"@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@*":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
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":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@@ -1176,6 +1186,20 @@
"@types/minimatch" "*"
"@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":
version "1.17.0"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.0.tgz#baf82ff6aa2723fd29f90e3ba1384e665006863e"
@@ -1183,6 +1207,13 @@
dependencies:
"@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":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -1215,6 +1246,11 @@
dependencies:
"@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":
version "3.12.1"
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"
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@*":
version "3.0.3"
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"
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":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz#99f026cbfdbe6305bdd454ffd5fefc1bd064beb1"
@@ -1338,6 +1391,13 @@
"@types/glob" "*"
"@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":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"