import Call from "@hapi/call"; import Subtext from "@hapi/subtext"; import http from "http"; import path from "path"; import { readFile } from "fs-extra"; import { Cluster } from "./cluster"; import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars"; import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes"; import logger from "./logger"; export interface RouterRequestOpts { req: http.IncomingMessage; res: http.ServerResponse; cluster: Cluster; params: RouteParams; url: URL; } export interface RouteParams extends Record { path?: string; // *-route namespace?: string; service?: string; account?: string; release?: string; repo?: string; chart?: string; } export interface LensApiRequest

{ path: string; payload: P; params: RouteParams; cluster: Cluster; response: http.ServerResponse; query: URLSearchParams; raw: { req: http.IncomingMessage; } } export class Router { protected router: any; public constructor() { this.router = new Call.Router(); this.addRoutes(); } public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { const url = new URL(req.url, "http://localhost"); const path = url.pathname; const method = req.method.toLowerCase(); const matchingRoute = this.router.route(method, path); const routeFound = !matchingRoute.isBoom; if (routeFound) { const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params }); await matchingRoute.route(request); return true; } return false; } protected async getRequest(opts: RouterRequestOpts): Promise { const { req, res, url, cluster, params } = opts; const { payload } = await Subtext.parse(req, null, { parse: true, output: "data", }); return { cluster, path: url.pathname, raw: { req, }, response: res, query: url.searchParams, payload, params }; } protected getMimeType(filename: string) { const mimeTypes: Record = { html: "text/html", txt: "text/plain", css: "text/css", gif: "image/gif", jpg: "image/jpeg", png: "image/png", svg: "image/svg+xml", js: "application/javascript", woff2: "font/woff2", ttf: "font/ttf" }; return mimeTypes[path.extname(filename).slice(1)] || "text/plain"; } async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage, retryCount = 0) { const asset = path.join(__static, filePath); try { const filename = path.basename(req.url); // redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support) const toWebpackDevServer = filename.includes(appName) || filename.includes("hot-update") || req.url.includes("sockjs-node"); if (isDevelopment && toWebpackDevServer) { const redirectLocation = `http://localhost:${webpackDevServerPort}` + req.url; res.statusCode = 307; res.setHeader("Location", redirectLocation); res.end(); return; } const data = await readFile(asset); res.setHeader("Content-Type", this.getMimeType(asset)); res.write(data); res.end(); } catch (err) { if (retryCount > 5) { logger.error("handleStaticFile:", err.toString()); res.statusCode = 404; res.end(); return; } this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1); } } protected addRoutes() { // Static assets this.router.add( { method: "get", path: "/{path*}" }, ({ params, response, raw: { req } }: LensApiRequest) => { this.handleStaticFile(params.path, response, req); }); this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute)); // Watch API this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute)); // Metrics API this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)); // Port-forward API this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute)); // Helm API this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmRoute.listCharts.bind(helmRoute)); this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmRoute.getChart.bind(helmRoute)); this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmRoute.getChartValues.bind(helmRoute)); this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmRoute.installChart.bind(helmRoute)); this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.updateRelease.bind(helmRoute)); this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmRoute.rollbackRelease.bind(helmRoute)); this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmRoute.listReleases.bind(helmRoute)); this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.getRelease.bind(helmRoute)); this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmRoute.getReleaseValues.bind(helmRoute)); this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmRoute.getReleaseHistory.bind(helmRoute)); this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.deleteRelease.bind(helmRoute)); // Resource Applier API this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierRoute.applyResource.bind(resourceApplierRoute)); } }