mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Make routes in back-end comply to Open Closed Principle (#4859)
* Make registration of back-end routes happen using injectable Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for list charts Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for get chart Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for get chart values Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for installing chart Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for updating release Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for rollbacking release Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for listing releases Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for getting release Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for getting release values Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for getting release history Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for deleting release Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove HelmRoute for not being used anymore Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for applying resource Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for patching resource Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for stopping current port forward Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for getting current port forward Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Consolidate start port forward route to use injection token for registering route Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for metrics Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for getting service account Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for getting version Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add global override for reading file to make it not required where not interesting Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable to setup route for serving static files Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make adding routes private for router Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make routes wait until all asynchronous stuff are done Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce healthy abstraction between back-end routes and route handlers Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make response of route typed Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix incorrect return value of updateRelease Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make typing of routes support synchronous values Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make code cleaner Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Rename test for accuracy Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add test for throwing route handler Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove duplicate license header Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make mechanism of creating HTTP response an implementation detail Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove not needed properties from test Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix code style Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix typing and codestyle Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix typing error Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix merge conflicts Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Organize all router related files under directory Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
772e879b81
commit
38af26efc9
@ -334,6 +334,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
"@typescript-eslint/parser": "^5.10.1",
|
"@typescript-eslint/parser": "^5.10.1",
|
||||||
"ansi_up": "^5.1.0",
|
"ansi_up": "^5.1.0",
|
||||||
|
"mock-http": "^1.1.0",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"circular-dependency-plugin": "^5.2.2",
|
"circular-dependency-plugin": "^5.2.2",
|
||||||
"cli-progress": "^3.10.0",
|
"cli-progress": "^3.10.0",
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
|
import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query";
|
||||||
|
|
||||||
export interface IMetrics {
|
export interface IMetrics {
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Router } from "../router";
|
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
|
||||||
app: {
|
|
||||||
getVersion: () => "99.99.99",
|
|
||||||
getName: () => "lens",
|
|
||||||
setName: jest.fn(),
|
|
||||||
setPath: jest.fn(),
|
|
||||||
getPath: () => "tmp",
|
|
||||||
getLocale: () => "en",
|
|
||||||
setLoginItemSettings: jest.fn(),
|
|
||||||
},
|
|
||||||
ipcMain: {
|
|
||||||
on: jest.fn(),
|
|
||||||
handle: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("Router", () => {
|
|
||||||
it("blocks path traversal attacks", async () => {
|
|
||||||
const response: any = {
|
|
||||||
statusCode: 200,
|
|
||||||
end: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await (Router as any).handleStaticFile({
|
|
||||||
params: {
|
|
||||||
path: "../index.ts",
|
|
||||||
},
|
|
||||||
response,
|
|
||||||
raw: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.statusCode).toEqual(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("serves files under static root", async () => {
|
|
||||||
const response: any = {
|
|
||||||
statusCode: 200,
|
|
||||||
write: jest.fn(),
|
|
||||||
setHeader: jest.fn(),
|
|
||||||
end: jest.fn(),
|
|
||||||
};
|
|
||||||
const req: any = {
|
|
||||||
url: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
await (Router as any).handleStaticFile({
|
|
||||||
params: {
|
|
||||||
path: "router.test.ts",
|
|
||||||
},
|
|
||||||
response,
|
|
||||||
raw: { req },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.statusCode).toEqual(200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
66
src/main/__test__/static-file-route.test.ts
Normal file
66
src/main/__test__/static-file-route.test.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { LensApiRequest, Route } from "../router/router";
|
||||||
|
import staticFileRouteInjectable from "../routes/static-file-route.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getVersion: () => "99.99.99",
|
||||||
|
getName: () => "lens",
|
||||||
|
setName: jest.fn(),
|
||||||
|
setPath: jest.fn(),
|
||||||
|
getPath: () => "tmp",
|
||||||
|
getLocale: () => "en",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
handle: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("static-file-route", () => {
|
||||||
|
let handleStaticFileRoute: Route<Buffer>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
handleStaticFileRoute = di.inject(staticFileRouteInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks path traversal attacks", async () => {
|
||||||
|
const request = {
|
||||||
|
params: {
|
||||||
|
path: "../index.ts",
|
||||||
|
},
|
||||||
|
raw: {},
|
||||||
|
} as LensApiRequest<any>;
|
||||||
|
|
||||||
|
const result = await handleStaticFileRoute.handler(request);
|
||||||
|
|
||||||
|
expect(result).toEqual({ statusCode: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serves files under static root", async () => {
|
||||||
|
const req: any = {
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
params: {
|
||||||
|
path: "router.test.ts",
|
||||||
|
},
|
||||||
|
raw: { req },
|
||||||
|
} as LensApiRequest<any>;
|
||||||
|
|
||||||
|
const result = await handleStaticFileRoute.handler(request);
|
||||||
|
|
||||||
|
expect(result).toEqual({ statusCode: 404 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -14,6 +14,7 @@ import appNameInjectable from "./app-paths/app-name/app-name.injectable";
|
|||||||
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
|
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
|
||||||
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
|
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
|
||||||
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
|
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
|
||||||
|
import readFileInjectable from "../common/fs/read-file.injectable";
|
||||||
import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
||||||
|
|
||||||
export const getDiForUnitTesting = (
|
export const getDiForUnitTesting = (
|
||||||
@ -53,6 +54,10 @@ export const getDiForUnitTesting = (
|
|||||||
di.override(readJsonFileInjectable, () => () => {
|
di.override(readJsonFileInjectable, () => () => {
|
||||||
throw new Error("Tried to read JSON file from file system without specifying explicit override.");
|
throw new Error("Tried to read JSON file from file system without specifying explicit override.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
di.override(readFileInjectable, () => () => {
|
||||||
|
throw new Error("Tried to read file from file system without specifying explicit override.");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return di;
|
return di;
|
||||||
|
|||||||
@ -90,9 +90,11 @@ export async function upgradeRelease(name: string, chart: string, values: any, n
|
|||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const output = await execHelm(args);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: await execHelm(args),
|
log: output,
|
||||||
release: getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
release: await getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
await fse.unlink(valuesFilePath);
|
await fse.unlink(valuesFilePath);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type http from "http";
|
|||||||
import spdy from "spdy";
|
import spdy from "spdy";
|
||||||
import type httpProxy from "http-proxy";
|
import type httpProxy from "http-proxy";
|
||||||
import { apiPrefix, apiKubePrefix } from "../common/vars";
|
import { apiPrefix, apiKubePrefix } from "../common/vars";
|
||||||
import type { Router } from "./router";
|
import type { Router } from "./router/router";
|
||||||
import type { ContextHandler } from "./context-handler/context-handler";
|
import type { ContextHandler } from "./context-handler/context-handler";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
@ -84,7 +84,7 @@ export class LensProxy extends Singleton {
|
|||||||
|
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
||||||
|
|
||||||
return socket.destroy();
|
return socket.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,197 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Call from "@hapi/call";
|
|
||||||
import Subtext from "@hapi/subtext";
|
|
||||||
import type http from "http";
|
|
||||||
import type httpProxy from "http-proxy";
|
|
||||||
import path from "path";
|
|
||||||
import { readFile } from "fs-extra";
|
|
||||||
import type { Cluster } from "../common/cluster/cluster";
|
|
||||||
import { apiPrefix, appName, publicPath } from "../common/vars";
|
|
||||||
import { HelmApiRoute, KubeconfigRoute, MetricsRoute, PortForwardRoute, ResourceApplierApiRoute, VersionRoute } 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<string, string> {
|
|
||||||
path?: string; // *-route
|
|
||||||
namespace?: string;
|
|
||||||
service?: string;
|
|
||||||
account?: string;
|
|
||||||
release?: string;
|
|
||||||
repo?: string;
|
|
||||||
chart?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LensApiRequest<P = any> {
|
|
||||||
path: string;
|
|
||||||
payload: P;
|
|
||||||
params: RouteParams;
|
|
||||||
cluster: Cluster;
|
|
||||||
response: http.ServerResponse;
|
|
||||||
query: URLSearchParams;
|
|
||||||
raw: {
|
|
||||||
req: http.IncomingMessage;
|
|
||||||
res: http.ServerResponse;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMimeType(filename: string) {
|
|
||||||
const mimeTypes: Record<string, string> = {
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
routePortForward: (request: LensApiRequest) => Promise<void>;
|
|
||||||
httpProxy?: httpProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Router {
|
|
||||||
protected router = new Call.Router();
|
|
||||||
protected static rootPath = path.resolve(__static);
|
|
||||||
|
|
||||||
public constructor(private dependencies: Dependencies) {
|
|
||||||
this.addRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
|
||||||
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<LensApiRequest> {
|
|
||||||
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, res,
|
|
||||||
},
|
|
||||||
response: res,
|
|
||||||
query: url.searchParams,
|
|
||||||
payload,
|
|
||||||
params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static async handleStaticFile({ params, response }: LensApiRequest): Promise<void> {
|
|
||||||
let filePath = params.path;
|
|
||||||
|
|
||||||
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
|
|
||||||
const asset = path.join(Router.rootPath, filePath);
|
|
||||||
const normalizedFilePath = path.resolve(asset);
|
|
||||||
|
|
||||||
if (!normalizedFilePath.startsWith(Router.rootPath)) {
|
|
||||||
response.statusCode = 404;
|
|
||||||
|
|
||||||
return response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await readFile(asset);
|
|
||||||
|
|
||||||
response.setHeader("Content-Type", getMimeType(asset));
|
|
||||||
response.write(data);
|
|
||||||
response.end();
|
|
||||||
} catch (err) {
|
|
||||||
if (retryCount > 5) {
|
|
||||||
logger.error("handleStaticFile:", err.toString());
|
|
||||||
response.statusCode = 404;
|
|
||||||
|
|
||||||
return response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath = `${publicPath}/${appName}.html`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addRoutes() {
|
|
||||||
// Static assets
|
|
||||||
if (this.dependencies.httpProxy) {
|
|
||||||
this.router.add({ method: "get", path: "/{path*}" }, (apiReq: LensApiRequest) => {
|
|
||||||
const { req, res } = apiReq.raw;
|
|
||||||
|
|
||||||
if (req.url === "/" || !req.url.startsWith("/build/")) {
|
|
||||||
req.url = `${publicPath}/${appName}.html`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dependencies.httpProxy.web(req, res, {
|
|
||||||
target: "http://127.0.0.1:8080",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.router.add({ method: "get", path: "/{path*}" }, Router.handleStaticFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.router.add({ method: "get", path: "/version" }, VersionRoute.getVersion);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, KubeconfigRoute.routeServiceAccountRoute);
|
|
||||||
|
|
||||||
// Metrics API
|
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
|
|
||||||
|
|
||||||
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, this.dependencies.routePortForward);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward);
|
|
||||||
this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
|
||||||
|
|
||||||
// Helm API
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, HelmApiRoute.listCharts);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, HelmApiRoute.getChart);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, HelmApiRoute.getChartValues);
|
|
||||||
|
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, HelmApiRoute.installChart);
|
|
||||||
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, HelmApiRoute.updateRelease);
|
|
||||||
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, HelmApiRoute.rollbackRelease);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, HelmApiRoute.listReleases);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, HelmApiRoute.getRelease);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, HelmApiRoute.getReleaseValues);
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, HelmApiRoute.getReleaseHistory);
|
|
||||||
this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, HelmApiRoute.deleteRelease);
|
|
||||||
|
|
||||||
// Resource Applier API
|
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.applyResource);
|
|
||||||
this.router.add({ method: "patch", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.patchResource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
src/main/router/parse-request.injectable.ts
Normal file
13
src/main/router/parse-request.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import Subtext from "@hapi/subtext";
|
||||||
|
|
||||||
|
const parseRequestInjectable = getInjectable({
|
||||||
|
id: "parse-http-request",
|
||||||
|
instantiate: () => Subtext.parse,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default parseRequestInjectable;
|
||||||
86
src/main/router/router-content-types.ts
Normal file
86
src/main/router/router-content-types.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LensApiResult } from "./router";
|
||||||
|
|
||||||
|
export interface LensApiResultContentType {
|
||||||
|
resultMapper: (result: LensApiResult<any>) => ({
|
||||||
|
statusCode: number;
|
||||||
|
content: any;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultMapperFor =
|
||||||
|
(contentType: string): LensApiResultContentType["resultMapper"] =>
|
||||||
|
({ response, error, statusCode= error ? 400 : 200, headers = {}}) => ({
|
||||||
|
statusCode,
|
||||||
|
content: error || response,
|
||||||
|
headers: { ...headers, "Content-Type": contentType },
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SupportedFileExtension = "json" | "txt" | "html" | "css" | "gif" | "jpg" | "png" | "svg" | "js" | "woff2" | "ttf";
|
||||||
|
|
||||||
|
export const contentTypes: Record<SupportedFileExtension, LensApiResultContentType> = {
|
||||||
|
json: {
|
||||||
|
resultMapper: (result) => {
|
||||||
|
const resultMapper = resultMapperFor("application/json");
|
||||||
|
|
||||||
|
const mappedResult = resultMapper(result);
|
||||||
|
|
||||||
|
const contentIsObject = typeof mappedResult.content === "object";
|
||||||
|
const contentIsBuffer = mappedResult.content instanceof Buffer;
|
||||||
|
const contentShouldBeStringified = contentIsObject && !contentIsBuffer;
|
||||||
|
|
||||||
|
const content = contentShouldBeStringified
|
||||||
|
? JSON.stringify(mappedResult.content)
|
||||||
|
: mappedResult.content;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mappedResult,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
txt: {
|
||||||
|
resultMapper: resultMapperFor("text/plain"),
|
||||||
|
},
|
||||||
|
|
||||||
|
html: {
|
||||||
|
resultMapper: resultMapperFor("text/html"),
|
||||||
|
},
|
||||||
|
|
||||||
|
css: {
|
||||||
|
resultMapper: resultMapperFor("text/css"),
|
||||||
|
},
|
||||||
|
|
||||||
|
gif: {
|
||||||
|
resultMapper: resultMapperFor("image/gif"),
|
||||||
|
},
|
||||||
|
|
||||||
|
jpg: {
|
||||||
|
resultMapper: resultMapperFor("image/jpeg"),
|
||||||
|
},
|
||||||
|
|
||||||
|
png: {
|
||||||
|
resultMapper: resultMapperFor("image/png"),
|
||||||
|
},
|
||||||
|
|
||||||
|
svg: {
|
||||||
|
resultMapper: resultMapperFor("image/svg+xml"),
|
||||||
|
},
|
||||||
|
|
||||||
|
js: {
|
||||||
|
resultMapper: resultMapperFor("application/javascript"),
|
||||||
|
},
|
||||||
|
|
||||||
|
woff2: {
|
||||||
|
resultMapper: resultMapperFor("font/woff2"),
|
||||||
|
},
|
||||||
|
|
||||||
|
ttf: {
|
||||||
|
resultMapper: resultMapperFor("font/ttf"),
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -2,23 +2,22 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
import { Route, Router } from "./router";
|
||||||
import httpProxy from "http-proxy";
|
import parseRequestInjectable from "./parse-request.injectable";
|
||||||
import { Router } from "../router";
|
|
||||||
import routePortForwardInjectable
|
export const routeInjectionToken = getInjectionToken<Route<any>>({
|
||||||
from "../routes/port-forward/route-port-forward/route-port-forward.injectable";
|
id: "route-injection-token",
|
||||||
|
});
|
||||||
|
|
||||||
const routerInjectable = getInjectable({
|
const routerInjectable = getInjectable({
|
||||||
id: "router",
|
id: "router",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
const routes = di.injectMany(routeInjectionToken);
|
||||||
const proxy = isDevelopment ? httpProxy.createProxy() : undefined;
|
|
||||||
|
|
||||||
return new Router({
|
return new Router(routes, {
|
||||||
routePortForward: di.inject(routePortForwardInjectable),
|
parseRequest: di.inject(parseRequestInjectable),
|
||||||
httpProxy: proxy,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
294
src/main/router/router.test.ts
Normal file
294
src/main/router/router.test.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import routerInjectable, { routeInjectionToken } from "./router.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import type { Router, RouteHandler, Route } from "./router";
|
||||||
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
|
import { Request } from "mock-http";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
import parseRequestInjectable from "./parse-request.injectable";
|
||||||
|
import { contentTypes } from "./router-content-types";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
|
describe("router", () => {
|
||||||
|
let router: Router;
|
||||||
|
let routeHandlerMock: AsyncFnMock<RouteHandler<any>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
routeHandlerMock = asyncFn();
|
||||||
|
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
mockFs();
|
||||||
|
|
||||||
|
di.override(parseRequestInjectable, () => () => Promise.resolve({ payload: "some-payload" }));
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
const injectable = getInjectable({
|
||||||
|
id: "some-route",
|
||||||
|
|
||||||
|
instantiate: () => ({
|
||||||
|
method: "get",
|
||||||
|
path: "/some-path",
|
||||||
|
handler: routeHandlerMock,
|
||||||
|
} as Route<any>),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
di.register(injectable);
|
||||||
|
|
||||||
|
router = di.inject(routerInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockFs.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when navigating to the route", () => {
|
||||||
|
let actualPromise: Promise<boolean>;
|
||||||
|
let clusterStub: Cluster;
|
||||||
|
let requestStub: Request;
|
||||||
|
let responseStub: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestStub = new Request({
|
||||||
|
url: "/some-path",
|
||||||
|
method: "get",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
responseStub = { end: jest.fn(), setHeader: jest.fn(), write: jest.fn(), statusCode: undefined };
|
||||||
|
|
||||||
|
clusterStub = {} as Cluster;
|
||||||
|
|
||||||
|
actualPromise = router.route(clusterStub, requestStub, responseStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls handler with the request", () => {
|
||||||
|
expect(routeHandlerMock).toHaveBeenCalledWith({
|
||||||
|
cluster: clusterStub,
|
||||||
|
params: {},
|
||||||
|
path: "/some-path",
|
||||||
|
payload: "some-payload",
|
||||||
|
query: expect.any(URLSearchParams),
|
||||||
|
raw: { req: requestStub, res: responseStub },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given no content-type is specified, when handler resolves, resolves with JSON", async () => {
|
||||||
|
await routeHandlerMock.resolve({ response: "some-response-from-route-handler" });
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.setHeader.mock.calls).toEqual([
|
||||||
|
["Content-Type", "application/json"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given JSON content-type is specified, when handler resolves with object, resolves with JSON", async () => {
|
||||||
|
await routeHandlerMock.resolve({ response: { some: "object" }});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.end).toHaveBeenCalledWith('{"some":"object"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when handler resolves without any result", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await routeHandlerMock.resolve(undefined);
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves as plain text", () => {
|
||||||
|
expect(responseStub.setHeader.mock.calls).toEqual([["Content-Type", "text/plain"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves with status code for no content", async () => {
|
||||||
|
expect(responseStub.statusCode).toBe(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves without content", async () => {
|
||||||
|
expect(responseStub.end.mock.calls).toEqual([[]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when handler rejects", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await routeHandlerMock.reject(new Error("some-error"));
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves as plain text", () => {
|
||||||
|
expect(responseStub.setHeader.mock.calls).toEqual([["Content-Type", "text/plain"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves with "500" status code', () => {
|
||||||
|
expect(responseStub.statusCode).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves with error", () => {
|
||||||
|
expect(responseStub.end).toHaveBeenCalledWith("Error: some-error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ contentType: "text/plain", contentTypeObject: contentTypes.txt },
|
||||||
|
{ contentType: "application/json", contentTypeObject: contentTypes.json },
|
||||||
|
{ contentType: "text/html", contentTypeObject: contentTypes.html },
|
||||||
|
{ contentType: "text/css", contentTypeObject: contentTypes.css },
|
||||||
|
{ contentType: "image/gif", contentTypeObject: contentTypes.gif },
|
||||||
|
{ contentType: "image/jpeg", contentTypeObject: contentTypes.jpg },
|
||||||
|
{ contentType: "image/png", contentTypeObject: contentTypes.png },
|
||||||
|
{ contentType: "image/svg+xml", contentTypeObject: contentTypes.svg },
|
||||||
|
{ contentType: "application/javascript", contentTypeObject: contentTypes.js },
|
||||||
|
{ contentType: "font/woff2", contentTypeObject: contentTypes.woff2 },
|
||||||
|
{ contentType: "font/ttf", contentTypeObject: contentTypes.ttf },
|
||||||
|
].forEach(scenario => {
|
||||||
|
describe(`given content type is "${scenario.contentType}", when handler resolves with response`, () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await routeHandlerMock.resolve({ response: "some-response", contentType: scenario.contentTypeObject });
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has content type specific headers", () => {
|
||||||
|
expect(responseStub.setHeader.mock.calls).toEqual([
|
||||||
|
["Content-Type", scenario.contentType],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to successful status code", () => {
|
||||||
|
expect(responseStub.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has response as body", () => {
|
||||||
|
expect(responseStub.end).toHaveBeenCalledWith("some-response");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`given content type is "${scenario.contentType}", when handler resolves with success and custom status code, defaults to "200" as status code`, async () => {
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
response: "some-response",
|
||||||
|
statusCode: 204,
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.statusCode).toBe(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`given content type is "${scenario.contentType}", when handler resolves with success but without status code, defaults to "200" as status code`, async () => {
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
response: "some-response",
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it(`given content type is "${scenario.contentType}", when handler resolves without response, has no body`, async () => {
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
response: undefined,
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.end.mock.calls).toEqual([[]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`given content type is "${scenario.contentType}", when handler resolves with error, has error as body`, async () => {
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
error: "some-error",
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.end).toHaveBeenCalledWith("some-error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`given content type is "${scenario.contentType}", when handler resolves with error and status code, has custom status code`, async () => {
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
error: "some-error",
|
||||||
|
statusCode: 414,
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.statusCode).toBe(414);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`given content type is "${scenario.contentType}", when handler resolves with error but without status code, defaults to "400" as status code`, async () => {
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
error: "some-error",
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`given content type is "${scenario.contentType}", when handler resolves custom headers, resolves with content type specific headers and custom headers`, async () => {
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
response: "irrelevant",
|
||||||
|
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "some-content-type-to-be-overridden",
|
||||||
|
"Some-Header": "some-header-value",
|
||||||
|
},
|
||||||
|
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
|
||||||
|
expect(responseStub.setHeader.mock.calls).toEqual([
|
||||||
|
["Content-Type", scenario.contentType],
|
||||||
|
["Some-Header", "some-header-value"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`given content type is "${scenario.contentType}", when handler resolves with binary content`, () => {
|
||||||
|
let responseBufferStub: Buffer;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
responseBufferStub = Buffer.from("some-binary-content");
|
||||||
|
|
||||||
|
await routeHandlerMock.resolve({
|
||||||
|
response: responseBufferStub,
|
||||||
|
contentType: scenario.contentTypeObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await actualPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes binary content to response", () => {
|
||||||
|
expect(responseStub.write).toHaveBeenCalledWith(responseBufferStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not end with the response", () => {
|
||||||
|
expect(responseStub.end.mock.calls[0]).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
196
src/main/router/router.ts
Normal file
196
src/main/router/router.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Call from "@hapi/call";
|
||||||
|
import type http from "http";
|
||||||
|
import type httpProxy from "http-proxy";
|
||||||
|
import { toPairs } from "lodash/fp";
|
||||||
|
import path from "path";
|
||||||
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
|
import type { LensApiResultContentType } from "./router-content-types";
|
||||||
|
import { contentTypes } from "./router-content-types";
|
||||||
|
|
||||||
|
// TODO: Import causes side effect, sets value for __static
|
||||||
|
import "../../common/vars";
|
||||||
|
|
||||||
|
export interface RouterRequestOpts {
|
||||||
|
req: http.IncomingMessage;
|
||||||
|
res: http.ServerResponse;
|
||||||
|
cluster: Cluster;
|
||||||
|
params: RouteParams;
|
||||||
|
url: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteParams extends Record<string, string> {
|
||||||
|
path?: string; // *-route
|
||||||
|
namespace?: string;
|
||||||
|
service?: string;
|
||||||
|
account?: string;
|
||||||
|
release?: string;
|
||||||
|
repo?: string;
|
||||||
|
chart?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LensApiRequest<P = any> {
|
||||||
|
path: string;
|
||||||
|
payload: P;
|
||||||
|
params: RouteParams;
|
||||||
|
cluster: Cluster;
|
||||||
|
query: URLSearchParams;
|
||||||
|
raw: {
|
||||||
|
req: http.IncomingMessage;
|
||||||
|
res: http.ServerResponse;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
parseRequest: (request: http.IncomingMessage, _: null, options: { parse: boolean; output: string }) => Promise<{ payload: any }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Router {
|
||||||
|
protected router = new Call.Router();
|
||||||
|
protected static rootPath = path.resolve(__static);
|
||||||
|
|
||||||
|
constructor(routes: Route<unknown>[], private dependencies: Dependencies) {
|
||||||
|
routes.forEach(route => {
|
||||||
|
this.router.add({ method: route.method, path: route.path }, handleRoute(route));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
||||||
|
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, res);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest> {
|
||||||
|
const { req, res, url, cluster, params } = opts;
|
||||||
|
|
||||||
|
const { payload } = await this.dependencies.parseRequest(req, null, {
|
||||||
|
parse: true,
|
||||||
|
output: "data",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cluster,
|
||||||
|
path: url.pathname,
|
||||||
|
raw: {
|
||||||
|
req, res,
|
||||||
|
},
|
||||||
|
query: url.searchParams,
|
||||||
|
payload,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LensApiResult<TResult> {
|
||||||
|
statusCode?: number;
|
||||||
|
response?: TResult;
|
||||||
|
error?: any;
|
||||||
|
contentType?: LensApiResultContentType;
|
||||||
|
headers?: { [name: string]: string };
|
||||||
|
proxy?: httpProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteHandler<TResponse> = (
|
||||||
|
request: LensApiRequest
|
||||||
|
) =>
|
||||||
|
| Promise<LensApiResult<TResponse>>
|
||||||
|
| Promise<void>
|
||||||
|
| LensApiResult<TResponse>
|
||||||
|
| void;
|
||||||
|
|
||||||
|
export interface Route<TResponse> {
|
||||||
|
path: string;
|
||||||
|
method: "get" | "post" | "put" | "patch" | "delete";
|
||||||
|
handler: RouteHandler<TResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRoute = (route: Route<unknown>) => async (request: LensApiRequest, response: http.ServerResponse) => {
|
||||||
|
let result: LensApiResult<any> | void;
|
||||||
|
|
||||||
|
const writeServerResponse = writeServerResponseFor(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await route.handler(request);
|
||||||
|
} catch(error) {
|
||||||
|
const mappedResult = contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 500,
|
||||||
|
error: error.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const mappedResult = contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 204,
|
||||||
|
response: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.proxy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = result.contentType || contentTypes.json;
|
||||||
|
|
||||||
|
const mappedResult = contentType.resultMapper(result);
|
||||||
|
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeServerResponseFor =
|
||||||
|
(serverResponse: http.ServerResponse) =>
|
||||||
|
({
|
||||||
|
statusCode,
|
||||||
|
content,
|
||||||
|
headers,
|
||||||
|
}: {
|
||||||
|
statusCode: number;
|
||||||
|
content: any;
|
||||||
|
headers: { [name: string]: string };
|
||||||
|
}) => {
|
||||||
|
serverResponse.statusCode = statusCode;
|
||||||
|
|
||||||
|
const headerPairs = toPairs<string>(headers);
|
||||||
|
|
||||||
|
headerPairs.forEach(([name, value]) => {
|
||||||
|
serverResponse.setHeader(name, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (content instanceof Buffer) {
|
||||||
|
serverResponse.write(content);
|
||||||
|
|
||||||
|
serverResponse.end();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
serverResponse.end(content);
|
||||||
|
} else {
|
||||||
|
serverResponse.end();
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
|
||||||
import { helmService } from "../helm/helm-service";
|
|
||||||
import logger from "../logger";
|
|
||||||
import { respondJson, respondText } from "../utils/http-responses";
|
|
||||||
import { getBoolean } from "../utils/parse-query";
|
|
||||||
|
|
||||||
export class HelmApiRoute {
|
|
||||||
static async listCharts(request: LensApiRequest) {
|
|
||||||
const { response } = request;
|
|
||||||
const charts = await helmService.listCharts();
|
|
||||||
|
|
||||||
respondJson(response, charts);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getChart(request: LensApiRequest) {
|
|
||||||
const { params, query, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
|
|
||||||
|
|
||||||
respondJson(response, chart);
|
|
||||||
} catch (error) {
|
|
||||||
respondText(response, error?.toString() || "Error getting chart", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getChartValues(request: LensApiRequest) {
|
|
||||||
const { params, query, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
|
|
||||||
|
|
||||||
respondJson(response, values);
|
|
||||||
} catch (error) {
|
|
||||||
respondText(response, error?.toString() || "Error getting chart values", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async installChart(request: LensApiRequest) {
|
|
||||||
const { payload, cluster, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await helmService.installChart(cluster, payload);
|
|
||||||
|
|
||||||
respondJson(response, result, 201);
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error installing chart", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async updateRelease(request: LensApiRequest) {
|
|
||||||
const { cluster, params, payload, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
|
|
||||||
|
|
||||||
respondJson(response, result);
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error updating chart", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async rollbackRelease(request: LensApiRequest) {
|
|
||||||
const { cluster, params, payload, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
|
|
||||||
|
|
||||||
response.end();
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error rolling back chart", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async listReleases(request: LensApiRequest) {
|
|
||||||
const { cluster, params, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await helmService.listReleases(cluster, params.namespace);
|
|
||||||
|
|
||||||
respondJson(response, result);
|
|
||||||
} catch(error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error listing release", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getRelease(request: LensApiRequest) {
|
|
||||||
const { cluster, params, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await helmService.getRelease(cluster, params.release, params.namespace);
|
|
||||||
|
|
||||||
respondJson(response, result);
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error getting release", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getReleaseValues(request: LensApiRequest) {
|
|
||||||
const { cluster, params: { namespace, release }, response, query } = request;
|
|
||||||
const all = getBoolean(query, "all");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await helmService.getReleaseValues(release, { cluster, namespace, all });
|
|
||||||
|
|
||||||
respondText(response, result);
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error getting release values", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getReleaseHistory(request: LensApiRequest) {
|
|
||||||
const { cluster, params, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
|
|
||||||
|
|
||||||
respondJson(response, result);
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error getting release history", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteRelease(request: LensApiRequest) {
|
|
||||||
const { cluster, params, response } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
|
|
||||||
|
|
||||||
respondJson(response, result);
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(error);
|
|
||||||
respondText(response, error?.toString() || "Error deleting release", 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
src/main/routes/helm/charts/get-chart-route.injectable.ts
Normal file
36
src/main/routes/helm/charts/get-chart-route.injectable.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { RawHelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||||
|
|
||||||
|
interface GetChartResponse {
|
||||||
|
readme: string;
|
||||||
|
versions: RawHelmChart[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChartRouteInjectable = getInjectable({
|
||||||
|
id: "get-chart-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<GetChartResponse> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/v2/charts/{repo}/{chart}`,
|
||||||
|
|
||||||
|
handler: async ({ params, query }) => ({
|
||||||
|
response: await helmService.getChart(
|
||||||
|
params.repo,
|
||||||
|
params.chart,
|
||||||
|
query.get("version"),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getChartRouteInjectable;
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
|
||||||
|
const getChartRouteValuesInjectable = getInjectable({
|
||||||
|
id: "get-chart-route-values",
|
||||||
|
|
||||||
|
instantiate: (): Route<string> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/v2/charts/{repo}/{chart}/values`,
|
||||||
|
|
||||||
|
handler: async ({
|
||||||
|
params,
|
||||||
|
query,
|
||||||
|
}) => ({
|
||||||
|
response: await helmService.getChartValues(
|
||||||
|
params.repo,
|
||||||
|
params.chart,
|
||||||
|
query.get("version"),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getChartRouteValuesInjectable;
|
||||||
26
src/main/routes/helm/charts/list-charts-route.injectable.ts
Normal file
26
src/main/routes/helm/charts/list-charts-route.injectable.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
|
||||||
|
const listChartsRouteInjectable = getInjectable({
|
||||||
|
id: "list-charts-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<any> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/v2/charts`,
|
||||||
|
|
||||||
|
handler: async () => ({
|
||||||
|
response: await helmService.listCharts(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default listChartsRouteInjectable;
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
const deleteReleaseRouteInjectable = getInjectable({
|
||||||
|
id: "delete-release-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<string> => ({
|
||||||
|
method: "delete",
|
||||||
|
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { cluster, params } = request;
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: await helmService.deleteRelease(
|
||||||
|
cluster,
|
||||||
|
params.release,
|
||||||
|
params.namespace,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default deleteReleaseRouteInjectable;
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
const getReleaseRouteHistoryInjectable = getInjectable({
|
||||||
|
id: "get-release-history-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<any> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/v2/releases/{namespace}/{release}/history`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { cluster, params } = request;
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: await helmService.getReleaseHistory(
|
||||||
|
cluster,
|
||||||
|
params.release,
|
||||||
|
params.namespace,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getReleaseRouteHistoryInjectable;
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
const getReleaseRouteInjectable = getInjectable({
|
||||||
|
id: "get-release-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<any> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { cluster, params } = request;
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: await helmService.getRelease(
|
||||||
|
cluster,
|
||||||
|
params.release,
|
||||||
|
params.namespace,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getReleaseRouteInjectable;
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { getBoolean } from "../../../utils/parse-query";
|
||||||
|
import { contentTypes } from "../../../router/router-content-types";
|
||||||
|
|
||||||
|
const getReleaseRouteValuesInjectable = getInjectable({
|
||||||
|
id: "get-release-values-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<string> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/v2/releases/{namespace}/{release}/values`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { cluster, params: { namespace, release }, query } = request;
|
||||||
|
const all = getBoolean(query, "all");
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: await helmService.getReleaseValues(release, {
|
||||||
|
cluster,
|
||||||
|
namespace,
|
||||||
|
all,
|
||||||
|
}),
|
||||||
|
|
||||||
|
contentType: contentTypes.txt,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getReleaseRouteValuesInjectable;
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
interface InstallChartResponse {
|
||||||
|
log: string;
|
||||||
|
release: { name: string; namespace: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const installChartRouteInjectable = getInjectable({
|
||||||
|
id: "install-chart-route",
|
||||||
|
|
||||||
|
instantiate: () : Route<InstallChartResponse> => ({
|
||||||
|
method: "post",
|
||||||
|
path: `${apiPrefix}/v2/releases`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { payload, cluster } = request;
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: await helmService.installChart(cluster, payload),
|
||||||
|
statusCode: 201,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default installChartRouteInjectable;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
const listReleasesRouteInjectable = getInjectable({
|
||||||
|
id: "list-releases-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<Record<string, any>> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/v2/releases/{namespace?}`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { cluster, params } = request;
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: await helmService.listReleases(cluster, params.namespace),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default listReleasesRouteInjectable;
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
const rollbackReleaseRouteInjectable = getInjectable({
|
||||||
|
id: "rollback-release-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<void> => ({
|
||||||
|
method: "put",
|
||||||
|
path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { cluster, params, payload } = request;
|
||||||
|
|
||||||
|
await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rollbackReleaseRouteInjectable;
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { apiPrefix } from "../../../../common/vars";
|
||||||
|
import type { Route } from "../../../router/router";
|
||||||
|
import { helmService } from "../../../helm/helm-service";
|
||||||
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
interface UpdateReleaseResponse {
|
||||||
|
log: string;
|
||||||
|
release: { name: string; namespace: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateReleaseRouteInjectable = getInjectable({
|
||||||
|
id: "update-release-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<UpdateReleaseResponse> => ({
|
||||||
|
method: "put",
|
||||||
|
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
|
||||||
|
|
||||||
|
handler: async ({
|
||||||
|
cluster,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
}) => ({
|
||||||
|
response: await helmService.updateRelease(
|
||||||
|
cluster,
|
||||||
|
params.release,
|
||||||
|
params.namespace,
|
||||||
|
payload,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateReleaseRouteInjectable;
|
||||||
@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./kubeconfig-route";
|
|
||||||
export * from "./metrics-route";
|
|
||||||
export * from "./port-forward-route";
|
|
||||||
export * from "./helm-route";
|
|
||||||
export * from "./resource-applier-route";
|
|
||||||
export * from "./version-route";
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
|
||||||
import { respondJson } from "../utils/http-responses";
|
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
|
||||||
import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
|
|
||||||
|
|
||||||
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
|
|
||||||
const tokenData = Buffer.from(secret.data["token"], "base64");
|
|
||||||
|
|
||||||
return {
|
|
||||||
"apiVersion": "v1",
|
|
||||||
"kind": "Config",
|
|
||||||
"clusters": [
|
|
||||||
{
|
|
||||||
"name": cluster.contextName,
|
|
||||||
"cluster": {
|
|
||||||
"server": cluster.apiUrl,
|
|
||||||
"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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class KubeconfigRoute {
|
|
||||||
static async routeServiceAccountRoute(request: LensApiRequest) {
|
|
||||||
const { params, response, cluster } = request;
|
|
||||||
const client = (await cluster.getProxyKubeconfig()).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);
|
|
||||||
|
|
||||||
respondJson(response, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import type { Route } from "../../router/router";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
|
import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
|
||||||
|
|
||||||
|
const getServiceAccountRouteInjectable = getInjectable({
|
||||||
|
id: "get-service-account-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<ReturnType<typeof generateKubeConfig>> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
|
||||||
|
|
||||||
|
handler: async (request) => {
|
||||||
|
const { params, cluster } = request;
|
||||||
|
const client = (await cluster.getProxyKubeconfig()).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;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { response: generateKubeConfig(params.account, secret, cluster) };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getServiceAccountRouteInjectable;
|
||||||
|
|
||||||
|
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
|
||||||
|
const tokenData = Buffer.from(secret.data["token"], "base64");
|
||||||
|
|
||||||
|
return {
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Config",
|
||||||
|
"clusters": [
|
||||||
|
{
|
||||||
|
"name": cluster.contextName,
|
||||||
|
"cluster": {
|
||||||
|
"server": cluster.apiUrl,
|
||||||
|
"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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,109 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
|
||||||
import { respondJson } from "../utils/http-responses";
|
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
|
||||||
import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../common/cluster-types";
|
|
||||||
import logger from "../logger";
|
|
||||||
import { getMetrics } from "../k8s-request";
|
|
||||||
import { PrometheusProviderRegistry } from "../prometheus";
|
|
||||||
|
|
||||||
export type IMetricsQuery = string | string[] | {
|
|
||||||
[metricName: string]: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is used for backoff retry tracking.
|
|
||||||
const ATTEMPTS = [false, false, false, false, true];
|
|
||||||
|
|
||||||
// prometheus metrics loader
|
|
||||||
async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record<string, string>): Promise<any[]> {
|
|
||||||
const queries = promQueries.map(p => p.trim());
|
|
||||||
const loaders = new Map<string, Promise<any>>();
|
|
||||||
|
|
||||||
async function loadMetric(query: string): Promise<any> {
|
|
||||||
async function loadMetricHelper(): Promise<any> {
|
|
||||||
for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry
|
|
||||||
try {
|
|
||||||
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
|
|
||||||
} catch (error) {
|
|
||||||
if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) {
|
|
||||||
logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error);
|
|
||||||
throw new Error("Metrics not available");
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(queries.map(loadMetric));
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MetricProviderInfo {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
isConfigurable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MetricsRoute {
|
|
||||||
static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
|
|
||||||
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
|
|
||||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails();
|
|
||||||
|
|
||||||
prometheusMetadata.provider = provider?.id;
|
|
||||||
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
|
||||||
|
|
||||||
if (!prometheusPath) {
|
|
||||||
prometheusMetadata.success = false;
|
|
||||||
|
|
||||||
return respondJson(response, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// return data in same structure as query
|
|
||||||
if (typeof payload === "string") {
|
|
||||||
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
|
|
||||||
|
|
||||||
respondJson(response, data);
|
|
||||||
} else if (Array.isArray(payload)) {
|
|
||||||
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
|
|
||||||
|
|
||||||
respondJson(response, data);
|
|
||||||
} else {
|
|
||||||
const queries = Object.entries<Record<string, string>>(payload)
|
|
||||||
.map(([queryName, queryOpts]) => (
|
|
||||||
provider.getQuery(queryOpts, queryName)
|
|
||||||
));
|
|
||||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
|
||||||
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
|
|
||||||
|
|
||||||
respondJson(response, data);
|
|
||||||
}
|
|
||||||
prometheusMetadata.success = true;
|
|
||||||
} catch (error) {
|
|
||||||
prometheusMetadata.success = false;
|
|
||||||
respondJson(response, {});
|
|
||||||
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error);
|
|
||||||
} finally {
|
|
||||||
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async routeMetricsProviders({ response }: LensApiRequest) {
|
|
||||||
const providers: MetricProviderInfo[] = [];
|
|
||||||
|
|
||||||
for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) {
|
|
||||||
providers.push({ name, id, isConfigurable });
|
|
||||||
}
|
|
||||||
|
|
||||||
respondJson(response, providers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
108
src/main/routes/metrics/add-metrics-route.injectable.ts
Normal file
108
src/main/routes/metrics/add-metrics-route.injectable.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import type { LensApiRequest, Route } from "../../router/router";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../../common/cluster-types";
|
||||||
|
import logger from "../../logger";
|
||||||
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
|
import { getMetrics } from "../../k8s-request";
|
||||||
|
import type { IMetricsQuery } from "./metrics-query";
|
||||||
|
|
||||||
|
// This is used for backoff retry tracking.
|
||||||
|
const ATTEMPTS = [false, false, false, false, true];
|
||||||
|
|
||||||
|
async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record<string, string>): Promise<any[]> {
|
||||||
|
const queries = promQueries.map(p => p.trim());
|
||||||
|
const loaders = new Map<string, Promise<any>>();
|
||||||
|
|
||||||
|
async function loadMetric(query: string): Promise<any> {
|
||||||
|
async function loadMetricHelper(): Promise<any> {
|
||||||
|
for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry
|
||||||
|
try {
|
||||||
|
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
|
||||||
|
} catch (error) {
|
||||||
|
if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) {
|
||||||
|
logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error);
|
||||||
|
throw new Error("Metrics not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(queries.map(loadMetric));
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMetricsRoute = async ({ cluster, payload, query }: LensApiRequest) => {
|
||||||
|
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
|
||||||
|
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails();
|
||||||
|
|
||||||
|
prometheusMetadata.provider = provider?.id;
|
||||||
|
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
||||||
|
|
||||||
|
if (!prometheusPath) {
|
||||||
|
prometheusMetadata.success = false;
|
||||||
|
|
||||||
|
return { response: {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// return data in same structure as query
|
||||||
|
if (typeof payload === "string") {
|
||||||
|
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
|
||||||
|
|
||||||
|
return { response: data };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
|
||||||
|
|
||||||
|
return { response: data };
|
||||||
|
}
|
||||||
|
|
||||||
|
const queries = Object.entries<Record<string, string>>(payload)
|
||||||
|
.map(([queryName, queryOpts]) => (
|
||||||
|
provider.getQuery(queryOpts, queryName)
|
||||||
|
));
|
||||||
|
|
||||||
|
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||||
|
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
|
||||||
|
|
||||||
|
prometheusMetadata.success = true;
|
||||||
|
|
||||||
|
return { response: data };
|
||||||
|
} catch (error) {
|
||||||
|
prometheusMetadata.success = false;
|
||||||
|
|
||||||
|
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error);
|
||||||
|
|
||||||
|
return { response: {}};
|
||||||
|
} finally {
|
||||||
|
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMetricsRouteInjectable = getInjectable({
|
||||||
|
id: "add-metrics-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<any> => ({
|
||||||
|
method: "post",
|
||||||
|
path: `${apiPrefix}/metrics`,
|
||||||
|
handler: addMetricsRoute,
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default addMetricsRouteInjectable;
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import type { Route } from "../../router/router";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import { PrometheusProviderRegistry } from "../../prometheus";
|
||||||
|
import type { MetricProviderInfo } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||||
|
|
||||||
|
const getMetricProvidersRouteInjectable = getInjectable({
|
||||||
|
id: "get-metric-providers-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<MetricProviderInfo[]> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/metrics/providers`,
|
||||||
|
|
||||||
|
handler: () => {
|
||||||
|
const providers: MetricProviderInfo[] = [];
|
||||||
|
|
||||||
|
for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) {
|
||||||
|
providers.push({ name, id, isConfigurable });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { response: providers };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getMetricProvidersRouteInjectable;
|
||||||
7
src/main/routes/metrics/metrics-query.ts
Normal file
7
src/main/routes/metrics/metrics-query.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
export type IMetricsQuery = string | string[] | {
|
||||||
|
[metricName: string]: string;
|
||||||
|
};
|
||||||
@ -1,48 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
|
||||||
import logger from "../logger";
|
|
||||||
import { respondJson } from "../utils/http-responses";
|
|
||||||
import { PortForward } from "./port-forward/port-forward";
|
|
||||||
|
|
||||||
export class PortForwardRoute {
|
|
||||||
static async routeCurrentPortForward(request: LensApiRequest) {
|
|
||||||
const { params, query, response, cluster } = request;
|
|
||||||
const { namespace, resourceType, resourceName } = params;
|
|
||||||
const port = Number(query.get("port"));
|
|
||||||
const forwardPort = Number(query.get("forwardPort"));
|
|
||||||
|
|
||||||
const portForward = PortForward.getPortforward({
|
|
||||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
|
||||||
namespace, port, forwardPort,
|
|
||||||
});
|
|
||||||
|
|
||||||
respondJson(response, { port: portForward?.forwardPort ?? null });
|
|
||||||
}
|
|
||||||
|
|
||||||
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
|
||||||
const { params, query, response, cluster } = request;
|
|
||||||
const { namespace, resourceType, resourceName } = params;
|
|
||||||
const port = Number(query.get("port"));
|
|
||||||
const forwardPort = Number(query.get("forwardPort"));
|
|
||||||
|
|
||||||
const portForward = PortForward.getPortforward({
|
|
||||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
|
||||||
namespace, port, forwardPort,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await portForward.stop();
|
|
||||||
respondJson(response, { status: true });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
|
|
||||||
|
|
||||||
return respondJson(response, {
|
|
||||||
message: `error stopping a forward port ${port}`,
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { PortForward, PortForwardArgs } from "./port-forward";
|
import { PortForward, PortForwardArgs } from "./port-forward";
|
||||||
import bundledKubectlInjectable from "../../kubectl/bundled-kubectl.injectable";
|
import bundledKubectlInjectable from "../../../kubectl/bundled-kubectl.injectable";
|
||||||
|
|
||||||
const createPortForwardInjectable = getInjectable({
|
const createPortForwardInjectable = getInjectable({
|
||||||
id: "create-port-forward",
|
id: "create-port-forward",
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import logger from "../../logger";
|
import logger from "../../../logger";
|
||||||
import { getPortFrom } from "../../utils/get-port";
|
import { getPortFrom } from "../../../utils/get-port";
|
||||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||||
import * as tcpPortUsed from "tcp-port-used";
|
import * as tcpPortUsed from "tcp-port-used";
|
||||||
|
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import type { LensApiRequest, Route } from "../../router/router";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import { PortForward } from "./functionality/port-forward";
|
||||||
|
|
||||||
|
const getCurrentPortForward = async (request: LensApiRequest) => {
|
||||||
|
const { params, query, cluster } = request;
|
||||||
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
|
const portForward = PortForward.getPortforward({
|
||||||
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
|
namespace, port, forwardPort,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { response: { port: portForward?.forwardPort ?? null }};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentPortForwardRouteInjectable = getInjectable({
|
||||||
|
id: "get-current-port-forward-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<{ port: number }> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
|
||||||
|
handler: getCurrentPortForward,
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getCurrentPortForwardRouteInjectable;
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { routePortForward } from "./route-port-forward";
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import createPortForwardInjectable from "../create-port-forward.injectable";
|
|
||||||
|
|
||||||
const routePortForwardInjectable = getInjectable({
|
|
||||||
id: "route-port-forward",
|
|
||||||
|
|
||||||
instantiate: (di) => routePortForward({
|
|
||||||
createPortForward: di.inject(createPortForwardInjectable),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default routePortForwardInjectable;
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { LensApiRequest } from "../../../router";
|
|
||||||
import logger from "../../../logger";
|
|
||||||
import { respondJson } from "../../../utils/http-responses";
|
|
||||||
import { PortForward, PortForwardArgs } from "../port-forward";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
createPortForward: (pathToKubeConfig: string, args: PortForwardArgs) => PortForward;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const routePortForward =
|
|
||||||
({ createPortForward }: Dependencies) =>
|
|
||||||
async (request: LensApiRequest) => {
|
|
||||||
const { params, query, response, cluster } = request;
|
|
||||||
const { namespace, resourceType, resourceName } = params;
|
|
||||||
const port = Number(query.get("port"));
|
|
||||||
const forwardPort = Number(query.get("forwardPort"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
let portForward = PortForward.getPortforward({
|
|
||||||
clusterId: cluster.id,
|
|
||||||
kind: resourceType,
|
|
||||||
name: resourceName,
|
|
||||||
namespace,
|
|
||||||
port,
|
|
||||||
forwardPort,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!portForward) {
|
|
||||||
logger.info(
|
|
||||||
`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const thePort =
|
|
||||||
0 < forwardPort && forwardPort < 65536 ? forwardPort : 0;
|
|
||||||
|
|
||||||
portForward = createPortForward(await cluster.getProxyKubeconfigPath(), {
|
|
||||||
clusterId: cluster.id,
|
|
||||||
kind: resourceType,
|
|
||||||
namespace,
|
|
||||||
name: resourceName,
|
|
||||||
port,
|
|
||||||
forwardPort: thePort,
|
|
||||||
});
|
|
||||||
|
|
||||||
const started = await portForward.start();
|
|
||||||
|
|
||||||
if (!started) {
|
|
||||||
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", {
|
|
||||||
namespace,
|
|
||||||
port,
|
|
||||||
resourceType,
|
|
||||||
resourceName,
|
|
||||||
});
|
|
||||||
|
|
||||||
return respondJson(
|
|
||||||
response,
|
|
||||||
{
|
|
||||||
message: `Failed to forward port ${port} to ${
|
|
||||||
thePort ? forwardPort : "random port"
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
respondJson(response, { port: portForward.forwardPort });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`,
|
|
||||||
{ namespace, port, resourceType, resourceName },
|
|
||||||
);
|
|
||||||
|
|
||||||
return respondJson(
|
|
||||||
response,
|
|
||||||
{
|
|
||||||
message: `Failed to forward port ${port}`,
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import type { LensApiRequest, Route } from "../../router/router";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import { PortForward, PortForwardArgs } from "./functionality/port-forward";
|
||||||
|
import logger from "../../logger";
|
||||||
|
import createPortForwardInjectable from "./functionality/create-port-forward.injectable";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
createPortForward: (pathToKubeConfig: string, args: PortForwardArgs) => PortForward;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPortForward = ({ createPortForward }: Dependencies) => async (request: LensApiRequest) => {
|
||||||
|
const { params, query, cluster } = request;
|
||||||
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
let portForward = PortForward.getPortforward({
|
||||||
|
clusterId: cluster.id,
|
||||||
|
kind: resourceType,
|
||||||
|
name: resourceName,
|
||||||
|
namespace,
|
||||||
|
port,
|
||||||
|
forwardPort,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!portForward) {
|
||||||
|
logger.info(
|
||||||
|
`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const thePort =
|
||||||
|
0 < forwardPort && forwardPort < 65536 ? forwardPort : 0;
|
||||||
|
|
||||||
|
portForward = createPortForward(await cluster.getProxyKubeconfigPath(), {
|
||||||
|
clusterId: cluster.id,
|
||||||
|
kind: resourceType,
|
||||||
|
namespace,
|
||||||
|
name: resourceName,
|
||||||
|
port,
|
||||||
|
forwardPort: thePort,
|
||||||
|
});
|
||||||
|
|
||||||
|
const started = await portForward.start();
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", {
|
||||||
|
namespace,
|
||||||
|
port,
|
||||||
|
resourceType,
|
||||||
|
resourceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: `Failed to forward port ${port} to ${
|
||||||
|
thePort ? forwardPort : "random port"
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { response: { port: portForward.forwardPort }};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`,
|
||||||
|
{ namespace, port, resourceType, resourceName },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: `Failed to forward port ${port}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPortForwardRouteInjectable = getInjectable({
|
||||||
|
id: "start-current-port-forward-route",
|
||||||
|
|
||||||
|
instantiate: (di): Route<{ port: number }> => ({
|
||||||
|
method: "post",
|
||||||
|
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
|
||||||
|
handler: startPortForward({ createPortForward: di.inject(createPortForwardInjectable) }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default startPortForwardRouteInjectable;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import type { LensApiRequest, Route } from "../../router/router";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import { PortForward } from "./functionality/port-forward";
|
||||||
|
import logger from "../../logger";
|
||||||
|
|
||||||
|
const stopCurrentPortForward = async (request: LensApiRequest) => {
|
||||||
|
const { params, query, cluster } = request;
|
||||||
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
|
const portForward = PortForward.getPortforward({
|
||||||
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
|
namespace, port, forwardPort,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await portForward.stop();
|
||||||
|
|
||||||
|
return { response: { status: true }};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: `error stopping a forward port ${port}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopCurrentPortForwardRouteInjectable = getInjectable({
|
||||||
|
id: "stop-current-port-forward-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<{ status: boolean }> => ({
|
||||||
|
method: "delete",
|
||||||
|
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
|
||||||
|
handler: stopCurrentPortForward,
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default stopCurrentPortForwardRouteInjectable;
|
||||||
@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
|
||||||
import { respondJson, respondText } from "../utils/http-responses";
|
|
||||||
import { ResourceApplier } from "../resource-applier";
|
|
||||||
|
|
||||||
export class ResourceApplierApiRoute {
|
|
||||||
static async applyResource(request: LensApiRequest) {
|
|
||||||
const { response, cluster, payload } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resource = await new ResourceApplier(cluster).apply(payload);
|
|
||||||
|
|
||||||
respondJson(response, resource, 200);
|
|
||||||
} catch (error) {
|
|
||||||
respondText(response, error, 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async patchResource(request: LensApiRequest) {
|
|
||||||
const { response, cluster, payload } = request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resource = await new ResourceApplier(cluster).patch(payload.name, payload.kind, payload.patch, payload.ns);
|
|
||||||
|
|
||||||
respondJson(response, resource, 200);
|
|
||||||
} catch (error) {
|
|
||||||
respondText(response, error, 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import type { Route } from "../../router/router";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import { ResourceApplier } from "../../resource-applier";
|
||||||
|
|
||||||
|
const applyResourceRouteInjectable = getInjectable({
|
||||||
|
id: "apply-resource-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<string> => ({
|
||||||
|
method: "post",
|
||||||
|
path: `${apiPrefix}/stack`,
|
||||||
|
|
||||||
|
handler: async ({ cluster, payload }) => ({
|
||||||
|
response: await new ResourceApplier(cluster).apply(payload),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default applyResourceRouteInjectable;
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import type { Route } from "../../router/router";
|
||||||
|
import { apiPrefix } from "../../../common/vars";
|
||||||
|
import { ResourceApplier } from "../../resource-applier";
|
||||||
|
|
||||||
|
const patchResourceRouteInjectable = getInjectable({
|
||||||
|
id: "patch-resource-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<string> => ({
|
||||||
|
method: "patch",
|
||||||
|
path: `${apiPrefix}/stack`,
|
||||||
|
|
||||||
|
handler: async ({ cluster, payload }) => ({
|
||||||
|
response: await new ResourceApplier(cluster).patch(
|
||||||
|
payload.name,
|
||||||
|
payload.kind,
|
||||||
|
payload.patch,
|
||||||
|
payload.ns,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default patchResourceRouteInjectable;
|
||||||
94
src/main/routes/static-file-route.injectable.ts
Normal file
94
src/main/routes/static-file-route.injectable.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { LensApiRequest, Route } from "../router/router";
|
||||||
|
import { contentTypes, SupportedFileExtension } from "../router/router-content-types";
|
||||||
|
import logger from "../logger";
|
||||||
|
import { routeInjectionToken } from "../router/router.injectable";
|
||||||
|
import { appName, publicPath } from "../../common/vars";
|
||||||
|
import path from "path";
|
||||||
|
import readFileInjectable from "../../common/fs/read-file.injectable";
|
||||||
|
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
||||||
|
import httpProxy from "http-proxy";
|
||||||
|
|
||||||
|
interface ProductionDependencies {
|
||||||
|
readFile: (path: string) => Promise<Buffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStaticFileInProduction =
|
||||||
|
({ readFile }: ProductionDependencies) =>
|
||||||
|
async ({ params }: LensApiRequest) => {
|
||||||
|
const staticPath = path.resolve(__static);
|
||||||
|
let filePath = params.path;
|
||||||
|
|
||||||
|
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
|
||||||
|
const asset = path.join(staticPath, filePath);
|
||||||
|
const normalizedFilePath = path.resolve(asset);
|
||||||
|
|
||||||
|
if (!normalizedFilePath.startsWith(staticPath)) {
|
||||||
|
return { statusCode: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileExtension = path
|
||||||
|
.extname(asset)
|
||||||
|
.slice(1) as SupportedFileExtension;
|
||||||
|
|
||||||
|
const contentType = contentTypes[fileExtension] || contentTypes.txt;
|
||||||
|
|
||||||
|
return { response: await readFile(asset), contentType };
|
||||||
|
} catch (err) {
|
||||||
|
if (retryCount > 5) {
|
||||||
|
logger.error("handleStaticFile:", err.toString());
|
||||||
|
|
||||||
|
return { statusCode: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = `${publicPath}/${appName}.html`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { statusCode: 404 };
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DevelopmentDependencies {
|
||||||
|
proxy: httpProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStaticFileInDevelopment =
|
||||||
|
({ proxy }: DevelopmentDependencies) =>
|
||||||
|
(apiReq: LensApiRequest) => {
|
||||||
|
const { req, res } = apiReq.raw;
|
||||||
|
|
||||||
|
if (req.url === "/" || !req.url.startsWith("/build/")) {
|
||||||
|
req.url = `${publicPath}/${appName}.html`;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.web(req, res, {
|
||||||
|
target: "http://127.0.0.1:8080",
|
||||||
|
});
|
||||||
|
|
||||||
|
return { proxy };
|
||||||
|
};
|
||||||
|
|
||||||
|
const staticFileRouteInjectable = getInjectable({
|
||||||
|
id: "static-file-route",
|
||||||
|
|
||||||
|
instantiate: (di): Route<Buffer> => {
|
||||||
|
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: "get",
|
||||||
|
path: `/{path*}`,
|
||||||
|
handler: isDevelopment
|
||||||
|
? handleStaticFileInDevelopment({ proxy: httpProxy.createProxy() })
|
||||||
|
: handleStaticFileInProduction({ readFile: di.inject(readFileInjectable) }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default staticFileRouteInjectable;
|
||||||
@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
|
||||||
import { respondJson } from "../utils/http-responses";
|
|
||||||
import { getAppVersion } from "../../common/utils";
|
|
||||||
|
|
||||||
export class VersionRoute {
|
|
||||||
static async getVersion(request: LensApiRequest) {
|
|
||||||
const { response } = request;
|
|
||||||
|
|
||||||
respondJson(response, { version: getAppVersion() }, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
src/main/routes/versions/get-version-route.injectable.ts
Normal file
23
src/main/routes/versions/get-version-route.injectable.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { Route } from "../../router/router";
|
||||||
|
import { routeInjectionToken } from "../../router/router.injectable";
|
||||||
|
import { getAppVersion } from "../../../common/utils";
|
||||||
|
|
||||||
|
const getVersionRouteInjectable = getInjectable({
|
||||||
|
id: "get-version-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<{ version: string }> => ({
|
||||||
|
method: "get",
|
||||||
|
path: `/version`,
|
||||||
|
|
||||||
|
handler: () => ({ response: { version: getAppVersion() }}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getVersionRouteInjectable;
|
||||||
12
yarn.lock
12
yarn.lock
@ -9047,6 +9047,11 @@ merge2@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||||
|
|
||||||
|
mergee@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mergee/-/mergee-1.0.0.tgz#027c5addc650f6ecbe4bf56100bd00dae763fda7"
|
||||||
|
integrity sha512-hbbXD4LOcxVkpS+mp3BMEhkSDf+lTVENFeEeqACgjjL8WrgKuW2EyLT0fOHyTbyDiuRLZJZ1HrHNeiX4iOd79Q==
|
||||||
|
|
||||||
methods@~1.1.2:
|
methods@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||||
@ -9290,6 +9295,13 @@ mock-fs@^5.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7"
|
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7"
|
||||||
integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A==
|
integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A==
|
||||||
|
|
||||||
|
mock-http@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mock-http/-/mock-http-1.1.0.tgz#b89380a718a103fc5801095804bedd0b20f7638c"
|
||||||
|
integrity sha512-H2HMGaHNQPWY8PdeEw4RFux2WEOHD6eJAtN3+iFELik5kGjPKAcoyPWcsC2vgDiTa2yimAEDssmMed51e+cBKQ==
|
||||||
|
dependencies:
|
||||||
|
mergee "^1.0.0"
|
||||||
|
|
||||||
moment-timezone@^0.5.34:
|
moment-timezone@^0.5.34:
|
||||||
version "0.5.34"
|
version "0.5.34"
|
||||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
|
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user