1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce healthy abstraction between back-end routes and route handlers

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-02-11 15:06:34 +02:00
parent d099256c9a
commit 4b76327489
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
23 changed files with 338 additions and 266 deletions

View File

@ -3,7 +3,7 @@
* 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 type { LensApiRequest } from "../router"; import type { LensApiRequest, LensApiResult } from "../router";
import staticFileRouteInjectable from "../routes/static-file-route.injectable"; import staticFileRouteInjectable from "../routes/static-file-route.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
@ -24,7 +24,7 @@ jest.mock("electron", () => ({
})); }));
describe("Router", () => { describe("Router", () => {
let handleStaticFile: (request: LensApiRequest) => Promise<void>; let handleStaticFile: (request: LensApiRequest) => Promise<LensApiResult>;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = getDiForUnitTesting({ doGeneralOverrides: true });

View File

@ -6,6 +6,8 @@
import Call from "@hapi/call"; import Call from "@hapi/call";
import Subtext from "@hapi/subtext"; import Subtext from "@hapi/subtext";
import type http from "http"; import type http from "http";
import type httpProxy from "http-proxy";
import { toPairs } from "lodash/fp";
import path from "path"; import path from "path";
import type { Cluster } from "../common/cluster/cluster"; import type { Cluster } from "../common/cluster/cluster";
@ -40,6 +42,94 @@ export interface LensApiRequest<P = any> {
}; };
} }
const respondFor =
(contentType: string) =>
(
content: any,
statusCode: number,
response: http.ServerResponse,
) => {
response.statusCode = statusCode;
response.setHeader("Content-Type", contentType);
if (content instanceof Buffer) {
response.write(content);
response.end();
return;
}
response.end(content);
};
export type SupportedFileExtension = "json" | "txt" | "html" | "css" | "gif" | "jpg" | "png" | "svg" | "js" | "woff2" | "ttf";
export const contentTypes: Record<SupportedFileExtension, LensApiResultContentType> = {
json: {
respond: (
content: any,
statusCode: number,
response: http.ServerResponse,
) => {
response.statusCode = statusCode;
response.setHeader("Content-Type", "application/json");
if (content instanceof Buffer) {
response.write(content);
response.end();
return;
}
const normalizedContent =
typeof content === "object" ? JSON.stringify(content) : content;
response.end(normalizedContent);
},
},
txt: {
respond: respondFor("text/plain"),
},
html: {
respond: respondFor("text/html"),
},
css: {
respond: respondFor("text/css"),
},
gif: {
respond: respondFor("image/gif"),
},
jpg: {
respond: respondFor("image/jpeg"),
},
png: {
respond: respondFor("image/png"),
},
svg: {
respond: respondFor("image/svg+xml"),
},
js: {
respond: respondFor("application/javascript"),
},
woff2: {
respond: respondFor("font/woff2"),
},
ttf: {
respond: respondFor("font/ttf"),
},
};
export class Router { export class Router {
protected router = new Call.Router(); protected router = new Call.Router();
static rootPath = path.resolve(__static); static rootPath = path.resolve(__static);
@ -47,7 +137,42 @@ export class Router {
constructor(routes: Route[]) { constructor(routes: Route[]) {
routes.forEach(route => { routes.forEach(route => {
this.router.add({ method: route.method, path: route.path }, async (request: LensApiRequest) => { this.router.add({ method: route.method, path: route.path }, async (request: LensApiRequest) => {
await route.handler(request); let result: LensApiResult | void;
try {
result = await route.handler(request);
} catch(error) {
contentTypes.txt.respond(error.toString(), 422, request.response);
return;
}
if (!result) {
contentTypes.txt.respond(null, 204, request.response);
return;
}
const {
response,
error,
statusCode = error ? 400 : 200,
contentType = contentTypes.json,
headers = {},
proxy,
} = result;
if (proxy) {
return;
}
const headerNameValuePairs = toPairs<string>(headers);
headerNameValuePairs.forEach(([key, value]) => {
request.response.setHeader(key, value);
});
contentType.respond(error || response, statusCode, request.response);
}); });
}); });
} }
@ -91,8 +216,21 @@ export class Router {
} }
} }
interface LensApiResultContentType {
respond: (content: any, statusCode: number, response: http.ServerResponse) => void;
}
export interface LensApiResult {
statusCode?: number;
response?: any;
error?: any;
contentType?: LensApiResultContentType;
headers?: { Location: string };
proxy?: httpProxy;
}
export interface Route { export interface Route {
path: string; path: string;
method: "get" | "post" | "put" | "patch" | "delete"; method: "get" | "post" | "put" | "patch" | "delete";
handler: (request: LensApiRequest) => void | Promise<void>; handler: (request: LensApiRequest) => LensApiResult | Promise<LensApiResult>;
} }

View File

@ -4,9 +4,8 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
const getChartRouteInjectable = getInjectable({ const getChartRouteInjectable = getInjectable({
@ -16,17 +15,16 @@ const getChartRouteInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}`, path: `${apiPrefix}/v2/charts/{repo}/{chart}`,
handler: async (request: LensApiRequest) => { handler: async ({
const { params, query, response } = request; params,
query,
try { }: LensApiRequest): Promise<LensApiResult> => ({
const chart = await helmService.getChart(params.repo, params.chart, query.get("version")); response: await helmService.getChart(
params.repo,
respondJson(response, chart); params.chart,
} catch (error) { query.get("version"),
respondText(response, error?.toString() || "Error getting chart", 422); ),
} }),
},
}), }),
injectionToken: routeInjectionToken, injectionToken: routeInjectionToken,

View File

@ -4,9 +4,8 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
const getChartRouteValuesInjectable = getInjectable({ const getChartRouteValuesInjectable = getInjectable({
@ -16,17 +15,16 @@ const getChartRouteValuesInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}/values`, path: `${apiPrefix}/v2/charts/{repo}/{chart}/values`,
handler: async (request: LensApiRequest) => { handler: async ({
const { params, query, response } = request; params,
query,
try { }: LensApiRequest): Promise<LensApiResult> => ({
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version")); response: await helmService.getChartValues(
params.repo,
respondJson(response, values); params.chart,
} catch (error) { query.get("version"),
respondText(response, error?.toString() || "Error getting chart values", 422); ),
} }),
},
}), }),
injectionToken: routeInjectionToken, injectionToken: routeInjectionToken,

View File

@ -4,9 +4,8 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson } from "../../../utils/http-responses";
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
const listChartsRouteInjectable = getInjectable({ const listChartsRouteInjectable = getInjectable({
@ -16,12 +15,10 @@ const listChartsRouteInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/v2/charts`, path: `${apiPrefix}/v2/charts`,
handler: async (request: LensApiRequest) => { // eslint-disable-next-line unused-imports/no-unused-vars-ts
const { response } = request; handler: async (request: LensApiRequest): Promise<LensApiResult> => ({
const charts = await helmService.listCharts(); response: await helmService.listCharts(),
}),
respondJson(response, charts);
},
}), }),
injectionToken: routeInjectionToken, injectionToken: routeInjectionToken,

View File

@ -3,12 +3,10 @@
* 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 { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
const deleteReleaseRouteInjectable = getInjectable({ const deleteReleaseRouteInjectable = getInjectable({
id: "delete-release-route", id: "delete-release-route",
@ -17,17 +15,16 @@ const deleteReleaseRouteInjectable = getInjectable({
method: "delete", method: "delete",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`, path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<LensApiResult> => {
const { cluster, params, response } = request; const { cluster, params } = request;
try { return {
const result = await helmService.deleteRelease(cluster, params.release, params.namespace); response: await helmService.deleteRelease(
cluster,
respondJson(response, result); params.release,
} catch (error) { params.namespace,
logger.debug(error); ),
respondText(response, error?.toString() || "Error deleting release", 422); };
}
}, },
}), }),

View File

@ -3,12 +3,10 @@
* 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 { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
const getReleaseRouteHistoryInjectable = getInjectable({ const getReleaseRouteHistoryInjectable = getInjectable({
id: "get-release-history-route", id: "get-release-history-route",
@ -17,17 +15,16 @@ const getReleaseRouteHistoryInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}/history`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/history`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<LensApiResult> => {
const { cluster, params, response } = request; const { cluster, params } = request;
try { return {
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace); response: await helmService.getReleaseHistory(
cluster,
respondJson(response, result); params.release,
} catch (error) { params.namespace,
logger.debug(error); ),
respondText(response, error?.toString() || "Error getting release history", 422); };
}
}, },
}), }),

View File

@ -3,12 +3,10 @@
* 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 { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
const getReleaseRouteInjectable = getInjectable({ const getReleaseRouteInjectable = getInjectable({
id: "get-release-route", id: "get-release-route",
@ -17,17 +15,16 @@ const getReleaseRouteInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`, path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<LensApiResult> => {
const { cluster, params, response } = request; const { cluster, params } = request;
try { return {
const result = await helmService.getRelease(cluster, params.release, params.namespace); response: await helmService.getRelease(
cluster,
respondJson(response, result); params.release,
} catch (error) { params.namespace,
logger.debug(error); ),
respondText(response, error?.toString() || "Error getting release", 422); };
}
}, },
}), }),

View File

@ -3,13 +3,12 @@
* 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 { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
import { getBoolean } from "../../../utils/parse-query"; import { getBoolean } from "../../../utils/parse-query";
import { contentTypes } from "../../../router";
const getReleaseRouteValuesInjectable = getInjectable({ const getReleaseRouteValuesInjectable = getInjectable({
id: "get-release-values-route", id: "get-release-values-route",
@ -18,18 +17,19 @@ const getReleaseRouteValuesInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}/values`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/values`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<LensApiResult> => {
const { cluster, params: { namespace, release }, response, query } = request; const { cluster, params: { namespace, release }, query } = request;
const all = getBoolean(query, "all"); const all = getBoolean(query, "all");
try { return {
const result = await helmService.getReleaseValues(release, { cluster, namespace, all }); response: await helmService.getReleaseValues(release, {
cluster,
namespace,
all,
}),
respondText(response, result); contentType: contentTypes.txt,
} catch (error) { };
logger.debug(error);
respondText(response, error?.toString() || "Error getting release values", 422);
}
}, },
}), }),

View File

@ -3,12 +3,10 @@
* 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 { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
const installChartRouteInjectable = getInjectable({ const installChartRouteInjectable = getInjectable({
id: "install-chart-route", id: "install-chart-route",
@ -17,17 +15,13 @@ const installChartRouteInjectable = getInjectable({
method: "post", method: "post",
path: `${apiPrefix}/v2/releases`, path: `${apiPrefix}/v2/releases`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<LensApiResult> => {
const { payload, cluster, response } = request; const { payload, cluster } = request;
try { return {
const result = await helmService.installChart(cluster, payload); response: await helmService.installChart(cluster, payload),
statusCode: 201,
respondJson(response, result, 201); };
} catch (error) {
logger.debug(error);
respondText(response, error?.toString() || "Error installing chart", 422);
}
}, },
}), }),

View File

@ -3,12 +3,10 @@
* 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 { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
const listReleasesRouteInjectable = getInjectable({ const listReleasesRouteInjectable = getInjectable({
id: "list-releases-route", id: "list-releases-route",
@ -17,17 +15,12 @@ const listReleasesRouteInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace?}`, path: `${apiPrefix}/v2/releases/{namespace?}`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<LensApiResult> => {
const { cluster, params, response } = request; const { cluster, params } = request;
try { return {
const result = await helmService.listReleases(cluster, params.namespace); response: await helmService.listReleases(cluster, params.namespace),
};
respondJson(response, result);
} catch(error) {
logger.debug(error);
respondText(response, error?.toString() || "Error listing release", 422);
}
}, },
}), }),

View File

@ -5,10 +5,8 @@
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
const rollbackReleaseRouteInjectable = getInjectable({ const rollbackReleaseRouteInjectable = getInjectable({
id: "rollback-release-route", id: "rollback-release-route",
@ -17,20 +15,14 @@ const rollbackReleaseRouteInjectable = getInjectable({
method: "put", method: "put",
path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<void> => {
const { cluster, params, payload, response } = request; const { cluster, params, payload } = request;
try { await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
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);
}
}, },
}), }),
// @ts-ignore
injectionToken: routeInjectionToken, injectionToken: routeInjectionToken,
}); });

View File

@ -3,12 +3,10 @@
* 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 { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { LensApiRequest } from "../../../router"; import type { LensApiRequest, LensApiResult } from "../../../router";
import { helmService } from "../../../helm/helm-service"; import { helmService } from "../../../helm/helm-service";
import { respondJson, respondText } from "../../../utils/http-responses";
import { routeInjectionToken } from "../../../router/router.injectable"; import { routeInjectionToken } from "../../../router/router.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import logger from "../../../logger";
const updateReleaseRouteInjectable = getInjectable({ const updateReleaseRouteInjectable = getInjectable({
id: "update-release-route", id: "update-release-route",
@ -17,18 +15,18 @@ const updateReleaseRouteInjectable = getInjectable({
method: "put", method: "put",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`, path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
handler: async (request: LensApiRequest) => { handler: async ({
const { cluster, params, payload, response } = request; cluster,
params,
try { payload,
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload ); }: LensApiRequest): Promise<LensApiResult> => ({
response: await helmService.updateRelease(
respondJson(response, result); cluster,
} catch (error) { params.release,
logger.debug(error); params.namespace,
respondText(response, error?.toString() || "Error updating chart", 422); payload,
} ),
}, }),
}), }),
injectionToken: routeInjectionToken, injectionToken: routeInjectionToken,

View File

@ -5,13 +5,11 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { respondJson } from "../../utils/http-responses";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"; import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
const getServiceAccountRouteInjectable = getInjectable({ const getServiceAccountRouteInjectable = getInjectable({
id: "get-service-account-route", id: "get-service-account-route",
@ -19,8 +17,8 @@ const getServiceAccountRouteInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`, path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
handler: async (request: LensApiRequest) => { handler: async (request: LensApiRequest): Promise<LensApiResult> => {
const { params, response, cluster } = request; const { params, cluster } = request;
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace); const secretList = await client.listNamespacedSecret(params.namespace);
@ -30,9 +28,7 @@ const getServiceAccountRouteInjectable = getInjectable({
return annotations && annotations["kubernetes.io/service-account.name"] == params.account; return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
}); });
const data = generateKubeConfig(params.account, secret, cluster); return { response: generateKubeConfig(params.account, secret, cluster) };
respondJson(response, data);
}, },
}), }),

View File

@ -5,8 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { respondJson } from "../../utils/http-responses";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../../common/cluster-types"; import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../../common/cluster-types";
import logger from "../../logger"; import logger from "../../logger";
@ -43,7 +42,7 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
return Promise.all(queries.map(loadMetric)); return Promise.all(queries.map(loadMetric));
} }
const addMetricsRoute = async ({ response, cluster, payload, query }: LensApiRequest) => { const addMetricsRoute = async ({ cluster, payload, query }: LensApiRequest): Promise<LensApiResult> => {
const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {}; const prometheusMetadata: ClusterPrometheusMetadata = {};
@ -56,33 +55,39 @@ const addMetricsRoute = async ({ response, cluster, payload, query }: LensApiReq
if (!prometheusPath) { if (!prometheusPath) {
prometheusMetadata.success = false; prometheusMetadata.success = false;
return respondJson(response, {}); return { response: {}};
} }
// return data in same structure as query // return data in same structure as query
if (typeof payload === "string") { if (typeof payload === "string") {
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams); const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
respondJson(response, data); return { response: data };
} else if (Array.isArray(payload)) { }
if (Array.isArray(payload)) {
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams); const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
respondJson(response, data); return { 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);
} }
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; prometheusMetadata.success = true;
return { response: data };
} catch (error) { } catch (error) {
prometheusMetadata.success = false; prometheusMetadata.success = false;
respondJson(response, {});
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error); logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error);
return { response: {}};
} finally { } finally {
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
} }

View File

@ -5,8 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { respondJson } from "../../utils/http-responses";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import { PrometheusProviderRegistry } from "../../prometheus"; import { PrometheusProviderRegistry } from "../../prometheus";
import type { MetricProviderInfo } from "../../../common/k8s-api/endpoints/metrics.api"; import type { MetricProviderInfo } from "../../../common/k8s-api/endpoints/metrics.api";
@ -18,14 +17,15 @@ const getMetricProvidersRouteInjectable = getInjectable({
method: "get", method: "get",
path: `${apiPrefix}/metrics/providers`, path: `${apiPrefix}/metrics/providers`,
handler: async (request: LensApiRequest) => { // eslint-disable-next-line unused-imports/no-unused-vars-ts
handler: async (request: LensApiRequest) : Promise<LensApiResult> => {
const providers: MetricProviderInfo[] = []; const providers: MetricProviderInfo[] = [];
for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) { for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) {
providers.push({ name, id, isConfigurable }); providers.push({ name, id, isConfigurable });
} }
respondJson(request.response, providers); return { response: providers };
}, },
}), }),

View File

@ -4,13 +4,12 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { respondJson } from "../../utils/http-responses";
import { PortForward } from "./functionality/port-forward"; import { PortForward } from "./functionality/port-forward";
const getCurrentPortForward = async (request: LensApiRequest) => { const getCurrentPortForward = async (request: LensApiRequest): Promise<LensApiResult> => {
const { params, query, response, cluster } = request; const { params, query, cluster } = request;
const { namespace, resourceType, resourceName } = params; const { namespace, resourceType, resourceName } = params;
const port = Number(query.get("port")); const port = Number(query.get("port"));
const forwardPort = Number(query.get("forwardPort")); const forwardPort = Number(query.get("forwardPort"));
@ -20,7 +19,7 @@ const getCurrentPortForward = async (request: LensApiRequest) => {
namespace, port, forwardPort, namespace, port, forwardPort,
}); });
respondJson(response, { port: portForward?.forwardPort ?? null }); return { response: { port: portForward?.forwardPort ?? null }};
}; };
const getCurrentPortForwardRouteInjectable = getInjectable({ const getCurrentPortForwardRouteInjectable = getInjectable({

View File

@ -4,19 +4,18 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { PortForward, PortForwardArgs } from "./functionality/port-forward"; import { PortForward, PortForwardArgs } from "./functionality/port-forward";
import logger from "../../logger"; import logger from "../../logger";
import { respondJson } from "../../utils/http-responses";
import createPortForwardInjectable from "./functionality/create-port-forward.injectable"; import createPortForwardInjectable from "./functionality/create-port-forward.injectable";
interface Dependencies { interface Dependencies {
createPortForward: (pathToKubeConfig: string, args: PortForwardArgs) => PortForward; createPortForward: (pathToKubeConfig: string, args: PortForwardArgs) => PortForward;
} }
const startPortForward = ({ createPortForward }: Dependencies) => async (request: LensApiRequest) => { const startPortForward = ({ createPortForward }: Dependencies) => async (request: LensApiRequest): Promise<LensApiResult> => {
const { params, query, response, cluster } = request; const { params, query, cluster } = request;
const { namespace, resourceType, resourceName } = params; const { namespace, resourceType, resourceName } = params;
const port = Number(query.get("port")); const port = Number(query.get("port"));
const forwardPort = Number(query.get("forwardPort")); const forwardPort = Number(query.get("forwardPort"));
@ -58,32 +57,28 @@ const startPortForward = ({ createPortForward }: Dependencies) => async (request
resourceName, resourceName,
}); });
return respondJson( return {
response, error: {
{
message: `Failed to forward port ${port} to ${ message: `Failed to forward port ${port} to ${
thePort ? forwardPort : "random port" thePort ? forwardPort : "random port"
}`, }`,
}, },
400, };
);
} }
} }
respondJson(response, { port: portForward.forwardPort }); return { response: { port: portForward.forwardPort }};
} catch (error) { } catch (error) {
logger.error( logger.error(
`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, `[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`,
{ namespace, port, resourceType, resourceName }, { namespace, port, resourceType, resourceName },
); );
return respondJson( return {
response, error: {
{
message: `Failed to forward port ${port}`, message: `Failed to forward port ${port}`,
}, },
400, };
);
} }
}; };

View File

@ -4,14 +4,13 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { respondJson } from "../../utils/http-responses";
import { PortForward } from "./functionality/port-forward"; import { PortForward } from "./functionality/port-forward";
import logger from "../../logger"; import logger from "../../logger";
const stopCurrentPortForward = async (request: LensApiRequest) => { const stopCurrentPortForward = async (request: LensApiRequest): Promise<LensApiResult> => {
const { params, query, response, cluster } = request; const { params, query, cluster } = request;
const { namespace, resourceType, resourceName } = params; const { namespace, resourceType, resourceName } = params;
const port = Number(query.get("port")); const port = Number(query.get("port"));
const forwardPort = Number(query.get("forwardPort")); const forwardPort = Number(query.get("forwardPort"));
@ -23,13 +22,17 @@ const stopCurrentPortForward = async (request: LensApiRequest) => {
try { try {
await portForward.stop(); await portForward.stop();
respondJson(response, { status: true });
return { response: { status: true }};
} catch (error) { } catch (error) {
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName }); logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
return respondJson(response, { return {
message: `error stopping a forward port ${port}`, error: {
}, 400); message: `error stopping a forward port ${port}`,
},
};
} }
}; };

View File

@ -4,10 +4,9 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { ResourceApplier } from "../../resource-applier"; import { ResourceApplier } from "../../resource-applier";
import { respondJson, respondText } from "../../utils/http-responses";
const applyResourceRouteInjectable = getInjectable({ const applyResourceRouteInjectable = getInjectable({
id: "apply-resource-route", id: "apply-resource-route",
@ -16,17 +15,12 @@ const applyResourceRouteInjectable = getInjectable({
method: "post", method: "post",
path: `${apiPrefix}/stack`, path: `${apiPrefix}/stack`,
handler: async (request: LensApiRequest) => { handler: async ({
const { response, cluster, payload } = request; cluster,
payload,
try { }: LensApiRequest): Promise<LensApiResult> => ({
const resource = await new ResourceApplier(cluster).apply(payload); response: await new ResourceApplier(cluster).apply(payload),
}),
respondJson(response, resource, 200);
} catch (error) {
respondText(response, error, 422);
}
},
}), }),
injectionToken: routeInjectionToken, injectionToken: routeInjectionToken,

View File

@ -4,10 +4,9 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { ResourceApplier } from "../../resource-applier"; import { ResourceApplier } from "../../resource-applier";
import { respondJson, respondText } from "../../utils/http-responses";
const patchResourceRouteInjectable = getInjectable({ const patchResourceRouteInjectable = getInjectable({
id: "patch-resource-route", id: "patch-resource-route",
@ -16,17 +15,17 @@ const patchResourceRouteInjectable = getInjectable({
method: "patch", method: "patch",
path: `${apiPrefix}/stack`, path: `${apiPrefix}/stack`,
handler: async (request: LensApiRequest) => { handler: async ({
const { response, cluster, payload } = request; cluster,
payload,
try { }: LensApiRequest): Promise<LensApiResult> => ({
const resource = await new ResourceApplier(cluster).patch(payload.name, payload.kind, payload.patch, payload.ns); response: await new ResourceApplier(cluster).patch(
payload.name,
respondJson(response, resource, 200); payload.kind,
} catch (error) { payload.patch,
respondText(response, error, 422); payload.ns,
} ),
}, }),
}), }),
injectionToken: routeInjectionToken, injectionToken: routeInjectionToken,

View File

@ -3,35 +3,15 @@
* 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 } from "@ogre-tools/injectable";
import type { LensApiRequest, Route } from "../router"; import type { LensApiRequest, LensApiResult, SupportedFileExtension, Route } from "../router";
import { contentTypes } from "../router";
import logger from "../logger"; import logger from "../logger";
import { routeInjectionToken } from "../router/router.injectable"; import { routeInjectionToken } from "../router/router.injectable";
import { import { appName, publicPath } from "../../common/vars";
appName,
publicPath,
} from "../../common/vars";
import path from "path"; import path from "path";
import readFileInjectable from "../../common/fs/read-file.injectable"; import readFileInjectable from "../../common/fs/read-file.injectable";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import httpProxy from "http-proxy"; import httpProxy from "http-proxy";
import { Router } from "../router";
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 ProductionDependencies { interface ProductionDependencies {
readFile: (path: string) => Promise<Buffer>; readFile: (path: string) => Promise<Buffer>;
@ -39,36 +19,38 @@ interface ProductionDependencies {
const handleStaticFileInProduction = const handleStaticFileInProduction =
({ readFile }: ProductionDependencies) => ({ readFile }: ProductionDependencies) =>
async ({ params, response }: LensApiRequest): Promise<void> => { async ({ params }: LensApiRequest): Promise<LensApiResult> => {
const staticPath = path.resolve(__static);
let filePath = params.path; let filePath = params.path;
for (let retryCount = 0; retryCount < 5; retryCount += 1) { for (let retryCount = 0; retryCount < 5; retryCount += 1) {
const asset = path.join(Router.rootPath, filePath); const asset = path.join(staticPath, filePath);
const normalizedFilePath = path.resolve(asset); const normalizedFilePath = path.resolve(asset);
if (!normalizedFilePath.startsWith(Router.rootPath)) { if (!normalizedFilePath.startsWith(staticPath)) {
response.statusCode = 404; return { statusCode: 404 };
return response.end();
} }
try { try {
const data = await readFile(asset); const fileExtension = path
.extname(asset)
.slice(1) as SupportedFileExtension;
response.setHeader("Content-Type", getMimeType(asset)); const contentType = contentTypes[fileExtension] || contentTypes.txt;
response.write(data);
response.end(); return { response: await readFile(asset), contentType };
} catch (err) { } catch (err) {
if (retryCount > 5) { if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString()); logger.error("handleStaticFile:", err.toString());
response.statusCode = 404;
return response.end(); return { statusCode: 404 };
} }
filePath = `${publicPath}/${appName}.html`; filePath = `${publicPath}/${appName}.html`;
} }
} }
return { statusCode: 404 };
}; };
interface DevelopmentDependencies { interface DevelopmentDependencies {
@ -87,6 +69,8 @@ const handleStaticFileInDevelopment =
proxy.web(req, res, { proxy.web(req, res, {
target: "http://127.0.0.1:8080", target: "http://127.0.0.1:8080",
}); });
return { proxy };
}; };
const staticFileRouteInjectable = getInjectable({ const staticFileRouteInjectable = getInjectable({

View File

@ -3,8 +3,7 @@
* 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 } from "@ogre-tools/injectable";
import type { LensApiRequest } from "../../router"; import type { LensApiRequest, LensApiResult } from "../../router";
import { respondJson } from "../../utils/http-responses";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import { getAppVersion } from "../../../common/utils"; import { getAppVersion } from "../../../common/utils";
@ -15,10 +14,9 @@ const getVersionRouteInjectable = getInjectable({
method: "get", method: "get",
path: `/version`, path: `/version`,
handler: async (request: LensApiRequest) => { // eslint-disable-next-line unused-imports/no-unused-vars-ts
const { response } = request; handler: async (request: LensApiRequest): Promise<LensApiResult> => {
return { response: { version: getAppVersion() }};
respondJson(response, { version: getAppVersion() }, 200);
}, },
}), }),