mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into lens-k8s-proxy-tls
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
commit
a3b7ebcfc1
19
package.json
19
package.json
@ -265,7 +265,7 @@
|
||||
"url-parse": "^1.5.10",
|
||||
"uuid": "^8.3.2",
|
||||
"win-ca": "^3.4.5",
|
||||
"winston": "^3.3.3",
|
||||
"winston": "^3.6.0",
|
||||
"winston-console-format": "^1.0.8",
|
||||
"winston-transport-browserconsole": "^1.0.5",
|
||||
"ws": "^7.5.5"
|
||||
@ -276,7 +276,7 @@
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@sentry/types": "^6.14.1",
|
||||
"@sentry/types": "^6.18.2",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@ -326,25 +326,26 @@
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"@types/url-parse": "^1.4.5",
|
||||
"@types/url-parse": "^1.4.8",
|
||||
"@types/uuid": "^8.3.3",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-dev-server": "^4.7.2",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"@types/webpack-node-externals": "^2.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||
"@typescript-eslint/parser": "^5.10.1",
|
||||
"ansi_up": "^5.1.0",
|
||||
"mock-http": "^1.1.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"cli-progress": "^3.10.0",
|
||||
"color": "^3.2.1",
|
||||
"concurrently": "^7.0.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"css-loader": "^6.7.1",
|
||||
"deepdash": "^5.3.9",
|
||||
"dompurify": "^2.3.4",
|
||||
"electron": "^14.2.4",
|
||||
"electron-builder": "^22.14.5",
|
||||
"electron-builder": "^22.14.13",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"esbuild": "^0.13.15",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
@ -379,11 +380,11 @@
|
||||
"react-refresh-typescript": "^2.0.3",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-select": "3.2.0",
|
||||
"react-select-event": "^5.1.0",
|
||||
"react-select-event": "^5.3.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-window": "^1.8.6",
|
||||
"sass": "^1.45.1",
|
||||
"sass-loader": "^12.4.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sharp": "^0.29.3",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.0.7",
|
||||
@ -391,7 +392,7 @@
|
||||
"ts-jest": "26.5.6",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.4.0",
|
||||
"type-fest": "^1.0.2",
|
||||
"type-fest": "^1.4.0",
|
||||
"typed-emitter": "^1.4.0",
|
||||
"typedoc": "0.22.10",
|
||||
"typedoc-plugin-markdown": "^3.11.12",
|
||||
|
||||
@ -89,8 +89,8 @@ describe("cluster-store", () => {
|
||||
mainDi = dis.mainDi;
|
||||
|
||||
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
mainDi.override(createKubeAuthProxyCertFilesInjectable, () => ({}));
|
||||
mainDi.override(kubeAuthProxyCaInjectable, () => ({}));
|
||||
mainDi.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||
mainDi.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||
|
||||
await dis.runSetups();
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import moment from "moment";
|
||||
import { apiBase } from "../index";
|
||||
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
|
||||
import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query";
|
||||
|
||||
export interface IMetrics {
|
||||
status: string;
|
||||
|
||||
@ -82,8 +82,8 @@ describe("ContextHandler", () => {
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
di.override(createKubeAuthProxyInjectable, () => ({}));
|
||||
di.override(kubeAuthProxyCaInjectable, () => ({}));
|
||||
di.override(createKubeAuthProxyInjectable, () => ({} as any));
|
||||
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
|
||||
@ -96,8 +96,8 @@ describe("kube auth proxy tests", () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(spawnInjectable, () => mockSpawn);
|
||||
di.override(createKubeAuthProxyCertFilesInjectable, () => ({}));
|
||||
di.override(kubeAuthProxyCaInjectable, () => ({}));
|
||||
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||
|
||||
mockFs(mockMinikubeConfig);
|
||||
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
@ -43,7 +43,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(kubeAuthProxyCaInjectable, () => Promise.resolve(Buffer.from("ca")));
|
||||
di.override(createKubeAuthProxyCertFilesInjectable, () => ({}));
|
||||
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||
|
||||
mockFs();
|
||||
|
||||
|
||||
@ -14,9 +14,9 @@ import appNameInjectable from "./app-paths/app-name/app-name.injectable";
|
||||
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
|
||||
import writeJsonFileInjectable from "../common/fs/write-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 spawnInjectable from "./child-process/spawn.injectable";
|
||||
import readFileInjectable from "../common/fs/read-file.injectable";
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
@ -64,7 +64,7 @@ export const getDiForUnitTesting = (
|
||||
});
|
||||
|
||||
di.override(readFileInjectable, () => () => {
|
||||
throw new Error("Tried to read a file from file system without specifying explicit override.");
|
||||
throw new Error("Tried to read file from file system without specifying explicit override.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -90,9 +90,11 @@ export async function upgradeRelease(name: string, chart: string, values: any, n
|
||||
];
|
||||
|
||||
try {
|
||||
const output = await execHelm(args);
|
||||
|
||||
return {
|
||||
log: await execHelm(args),
|
||||
release: getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
||||
log: output,
|
||||
release: await getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
||||
};
|
||||
} finally {
|
||||
await fse.unlink(valuesFilePath);
|
||||
|
||||
@ -8,7 +8,7 @@ import type http from "http";
|
||||
import spdy from "spdy";
|
||||
import type httpProxy from "http-proxy";
|
||||
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 logger from "./logger";
|
||||
import { Singleton } from "../common/utils";
|
||||
@ -84,7 +84,7 @@ export class LensProxy extends Singleton {
|
||||
|
||||
if (!cluster) {
|
||||
logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
||||
|
||||
|
||||
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.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
||||
import httpProxy from "http-proxy";
|
||||
import { Router } from "../router";
|
||||
import routePortForwardInjectable
|
||||
from "../routes/port-forward/route-port-forward/route-port-forward.injectable";
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import { Route, Router } from "./router";
|
||||
import parseRequestInjectable from "./parse-request.injectable";
|
||||
|
||||
export const routeInjectionToken = getInjectionToken<Route<any>>({
|
||||
id: "route-injection-token",
|
||||
});
|
||||
|
||||
const routerInjectable = getInjectable({
|
||||
id: "router",
|
||||
|
||||
instantiate: (di) => {
|
||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||
const proxy = isDevelopment ? httpProxy.createProxy() : undefined;
|
||||
const routes = di.injectMany(routeInjectionToken);
|
||||
|
||||
return new Router({
|
||||
routePortForward: di.inject(routePortForwardInjectable),
|
||||
httpProxy: proxy,
|
||||
return new Router(routes, {
|
||||
parseRequest: di.inject(parseRequestInjectable),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
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 { PortForward, PortForwardArgs } from "./port-forward";
|
||||
import bundledKubectlInjectable from "../../kubectl/bundled-kubectl.injectable";
|
||||
import bundledKubectlInjectable from "../../../kubectl/bundled-kubectl.injectable";
|
||||
|
||||
const createPortForwardInjectable = getInjectable({
|
||||
id: "create-port-forward",
|
||||
@ -2,8 +2,8 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import logger from "../../logger";
|
||||
import { getPortFrom } from "../../utils/get-port";
|
||||
import logger from "../../../logger";
|
||||
import { getPortFrom } from "../../../utils/get-port";
|
||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||
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;
|
||||
@ -58,7 +58,7 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
|
||||
{since && (
|
||||
<span>
|
||||
Logs from{" "}
|
||||
<b>{new Date(since[0]).toLocaleString()}</b>
|
||||
<b>{new Date(since).toLocaleString()}</b>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -68,67 +68,76 @@ export class ItemListLayoutContent<I extends ItemObject> extends React.Component
|
||||
return this.props.store.failedLoading;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
renderRow(item: I) {
|
||||
return this.getTableRow(item);
|
||||
}
|
||||
|
||||
getTableRow(item: I) {
|
||||
const {
|
||||
isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
|
||||
store, hasDetailsView, onDetails,
|
||||
copyClassNameFromHeadCells, customizeTableRowProps, detailsItem,
|
||||
} = this.props;
|
||||
const { isSelected } = store;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
nowrap
|
||||
searchItem={item}
|
||||
sortItem={item}
|
||||
selected={detailsItem && detailsItem.getId() === item.getId()}
|
||||
onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined}
|
||||
{...customizeTableRowProps(item)}
|
||||
>
|
||||
{isSelectable && (
|
||||
<TableCell
|
||||
checkbox
|
||||
isChecked={isSelected(item)}
|
||||
onClick={prevDefault(() => store.toggleSelection(item))}
|
||||
/>
|
||||
)}
|
||||
{renderTableContents(item).map((content, index) => {
|
||||
const cellProps: TableCellProps = isReactNode(content)
|
||||
? { children: content }
|
||||
: content;
|
||||
const headCell = renderTableHeader?.[index];
|
||||
|
||||
if (copyClassNameFromHeadCells && headCell) {
|
||||
cellProps.className = cssNames(
|
||||
cellProps.className,
|
||||
headCell.className,
|
||||
);
|
||||
}
|
||||
|
||||
if (!headCell || this.showColumn(headCell)) {
|
||||
return <TableCell key={index} {...cellProps} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{renderItemMenu && (
|
||||
<TableCell className="menu">
|
||||
<div onClick={stopPropagation}>
|
||||
{renderItemMenu(item, store)}
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
getRow(uid: string) {
|
||||
return (
|
||||
<div key={uid}>
|
||||
<Observer>
|
||||
{() => {
|
||||
const {
|
||||
isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
|
||||
store, hasDetailsView, onDetails,
|
||||
copyClassNameFromHeadCells, customizeTableRowProps, detailsItem,
|
||||
} = this.props;
|
||||
const { isSelected } = store;
|
||||
const item = this.props.getItems().find(item => item.getId() == uid);
|
||||
const item = this.props.getItems().find(item => item.getId() === uid);
|
||||
|
||||
if (!item) return null;
|
||||
const itemId = item.getId();
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
nowrap
|
||||
searchItem={item}
|
||||
sortItem={item}
|
||||
selected={detailsItem && detailsItem.getId() === itemId}
|
||||
onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined}
|
||||
{...customizeTableRowProps(item)}
|
||||
>
|
||||
{isSelectable && (
|
||||
<TableCell
|
||||
checkbox
|
||||
isChecked={isSelected(item)}
|
||||
onClick={prevDefault(() => store.toggleSelection(item))}
|
||||
/>
|
||||
)}
|
||||
{renderTableContents(item).map((content, index) => {
|
||||
const cellProps: TableCellProps = isReactNode(content)
|
||||
? { children: content }
|
||||
: content;
|
||||
const headCell = renderTableHeader?.[index];
|
||||
|
||||
if (copyClassNameFromHeadCells && headCell) {
|
||||
cellProps.className = cssNames(
|
||||
cellProps.className,
|
||||
headCell.className,
|
||||
);
|
||||
}
|
||||
|
||||
if (!headCell || this.showColumn(headCell)) {
|
||||
return <TableCell key={index} {...cellProps} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{renderItemMenu && (
|
||||
<TableCell className="menu">
|
||||
<div onClick={stopPropagation}>
|
||||
{renderItemMenu(item, store)}
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
return this.getTableRow(item);
|
||||
}}
|
||||
</Observer>
|
||||
</div>
|
||||
@ -248,6 +257,7 @@ export class ItemListLayoutContent<I extends ItemObject> extends React.Component
|
||||
selectable={hasDetailsView}
|
||||
sortable={sortingCallbacks}
|
||||
getTableRow={this.getRow}
|
||||
renderRow={virtual ? undefined : this.renderRow}
|
||||
items={items}
|
||||
selectedItemId={selectedItemId}
|
||||
noItems={this.renderNoItems()}
|
||||
|
||||
@ -75,10 +75,6 @@ export const initClusterFrame =
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("online", () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
logger.info(
|
||||
`${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`,
|
||||
|
||||
342
yarn.lock
342
yarn.lock
@ -330,6 +330,11 @@
|
||||
exec-sh "^0.3.2"
|
||||
minimist "^1.2.0"
|
||||
|
||||
"@colors/colors@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@cspotcode/source-map-consumer@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
|
||||
@ -1083,7 +1088,7 @@
|
||||
"@sentry/utils" "6.7.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@6.15.0", "@sentry/types@^6.14.1":
|
||||
"@sentry/types@6.15.0":
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.15.0.tgz#a2917f8aed91471bdfd6651384ffcd47b95c43ad"
|
||||
integrity sha512-zBw5gPUsofXUSpS3ZAXqRNedLRBvirl3sqkj2Lez7X2EkKRgn5D8m9fQIrig/X3TsKcXUpijDW5Buk5zeCVzJA==
|
||||
@ -1093,6 +1098,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.7.1.tgz#c8263e1886df5e815570c4668eb40a1cfaa1c88b"
|
||||
integrity sha512-9AO7HKoip2MBMNQJEd6+AKtjj2+q9Ze4ooWUdEvdOVSt5drg7BGpK221/p9JEOyJAZwEPEXdcMd3VAIMiOb4MA==
|
||||
|
||||
"@sentry/types@^6.18.2":
|
||||
version "6.18.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.18.2.tgz#f528fec8b75c19d5a6976004e71703184c6cf7be"
|
||||
integrity sha512-WzpJf/Q5aORTzrSwer/As1NlO90dBAQpaHV2ikDDKqOyMWEgjKb5/4gh59p9gH8JMMnLetP1AvQel0fOj5UnUw==
|
||||
|
||||
"@sentry/utils@6.15.0":
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.15.0.tgz#0c247cb092b1796d39c3d16d8e6977b9cdab9ca2"
|
||||
@ -1227,6 +1237,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
|
||||
@ -2031,10 +2046,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.0.tgz#5cb0dff2a5f616fc8e0c61b482bf01fa20a03cec"
|
||||
integrity sha512-ZAbqul7QAKpM2h1PFGa5ETN27ulmqtj0QviYHasw9LffvXZvVHuraOx/FOsIPPDNGZN0Qo1nASxxSfMYOtSoCw==
|
||||
|
||||
"@types/url-parse@^1.4.5":
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.5.tgz#bda0b813cc8f79d767dc8ca645fdabc238c6ae77"
|
||||
integrity sha512-8Wje3itJpk/FX+QItca9vjNLjGx5jlEYBw/CpMi03Fphk2DSVeZDUqWTE81BeCI5Bl6Z+zmA1O9L/8e3ZUSeLg==
|
||||
"@types/url-parse@^1.4.8":
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.8.tgz#c3825047efbca1295b7f1646f38203d9145130d6"
|
||||
integrity sha512-zqqcGKyNWgTLFBxmaexGUKQyWqeG7HjXj20EuQJSJWwXe54BjX0ihIo5cJB9yAQzH8dNugJ9GvkBYMjPXs/PJw==
|
||||
|
||||
"@types/uuid@^8.3.3":
|
||||
version "8.3.3"
|
||||
@ -2143,14 +2158,14 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.10.1":
|
||||
version "5.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.1.tgz#870195d0f2146b36d11fc71131b75aba52354c69"
|
||||
integrity sha512-xN3CYqFlyE/qOcy978/L0xLR2HlcAGIyIK5sMOasxaaAPfQRj/MmMV6OC3I7NZO84oEUdWCOju34Z9W8E0pFDQ==
|
||||
"@typescript-eslint/eslint-plugin@^5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.14.0.tgz#5119b67152356231a0e24b998035288a9cd21335"
|
||||
integrity sha512-ir0wYI4FfFUDfLcuwKzIH7sMVA+db7WYen47iRSaCGl+HMAZI9fpBwfDo45ZALD3A45ZGyHWDNLhbg8tZrMX4w==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.10.1"
|
||||
"@typescript-eslint/type-utils" "5.10.1"
|
||||
"@typescript-eslint/utils" "5.10.1"
|
||||
"@typescript-eslint/scope-manager" "5.14.0"
|
||||
"@typescript-eslint/type-utils" "5.14.0"
|
||||
"@typescript-eslint/utils" "5.14.0"
|
||||
debug "^4.3.2"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
ignore "^5.1.8"
|
||||
@ -2176,12 +2191,20 @@
|
||||
"@typescript-eslint/types" "5.10.1"
|
||||
"@typescript-eslint/visitor-keys" "5.10.1"
|
||||
|
||||
"@typescript-eslint/type-utils@5.10.1":
|
||||
version "5.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.10.1.tgz#5e526c00142585e40ab1503e83f1ff608c367405"
|
||||
integrity sha512-AfVJkV8uck/UIoDqhu+ptEdBoQATON9GXnhOpPLzkQRJcSChkvD//qsz9JVffl2goxX+ybs5klvacE9vmrQyCw==
|
||||
"@typescript-eslint/scope-manager@5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.14.0.tgz#ea518962b42db8ed0a55152ea959c218cb53ca7b"
|
||||
integrity sha512-LazdcMlGnv+xUc5R4qIlqH0OWARyl2kaP8pVCS39qSL3Pd1F7mI10DbdXeARcE62sVQE4fHNvEqMWsypWO+yEw==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "5.10.1"
|
||||
"@typescript-eslint/types" "5.14.0"
|
||||
"@typescript-eslint/visitor-keys" "5.14.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.14.0.tgz#711f08105860b12988454e91df433567205a8f0b"
|
||||
integrity sha512-d4PTJxsqaUpv8iERTDSQBKUCV7Q5yyXjqXUl3XF7Sd9ogNLuKLkxz82qxokqQ4jXdTPZudWpmNtr/JjbbvUixw==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "5.14.0"
|
||||
debug "^4.3.2"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
@ -2190,6 +2213,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.1.tgz#dca9bd4cb8c067fc85304a31f38ec4766ba2d1ea"
|
||||
integrity sha512-ZvxQ2QMy49bIIBpTqFiOenucqUyjTQ0WNLhBM6X1fh1NNlYAC6Kxsx8bRTY3jdYsYg44a0Z/uEgQkohbR0H87Q==
|
||||
|
||||
"@typescript-eslint/types@5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.14.0.tgz#96317cf116cea4befabc0defef371a1013f8ab11"
|
||||
integrity sha512-BR6Y9eE9360LNnW3eEUqAg6HxS9Q35kSIs4rp4vNHRdfg0s+/PgHgskvu5DFTM7G5VKAVjuyaN476LCPrdA7Mw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.10.1":
|
||||
version "5.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.1.tgz#b268e67be0553f8790ba3fe87113282977adda15"
|
||||
@ -2203,15 +2231,28 @@
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.10.1":
|
||||
version "5.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.10.1.tgz#fa682a33af47080ba2c4368ee0ad2128213a1196"
|
||||
integrity sha512-RRmlITiUbLuTRtn/gcPRi4202niF+q7ylFLCKu4c+O/PcpRvZ/nAUwQ2G00bZgpWkhrNLNnvhZLbDn8Ml0qsQw==
|
||||
"@typescript-eslint/typescript-estree@5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.14.0.tgz#78b7f7385d5b6f2748aacea5c9b7f6ae62058314"
|
||||
integrity sha512-QGnxvROrCVtLQ1724GLTHBTR0lZVu13izOp9njRvMkCBgWX26PKvmMP8k82nmXBRD3DQcFFq2oj3cKDwr0FaUA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.14.0"
|
||||
"@typescript-eslint/visitor-keys" "5.14.0"
|
||||
debug "^4.3.2"
|
||||
globby "^11.0.4"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.14.0.tgz#6c8bc4f384298cbbb32b3629ba7415f9f80dc8c4"
|
||||
integrity sha512-EHwlII5mvUA0UsKYnVzySb/5EE/t03duUTweVy8Zqt3UQXBrpEVY144OTceFKaOe4xQXZJrkptCf7PjEBeGK4w==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@typescript-eslint/scope-manager" "5.10.1"
|
||||
"@typescript-eslint/types" "5.10.1"
|
||||
"@typescript-eslint/typescript-estree" "5.10.1"
|
||||
"@typescript-eslint/scope-manager" "5.14.0"
|
||||
"@typescript-eslint/types" "5.14.0"
|
||||
"@typescript-eslint/typescript-estree" "5.14.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
@ -2223,6 +2264,14 @@
|
||||
"@typescript-eslint/types" "5.10.1"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.14.0.tgz#1927005b3434ccd0d3ae1b2ecf60e65943c36986"
|
||||
integrity sha512-yL0XxfzR94UEkjBqyymMLgCBdojzEuy/eim7N9/RIcTNxpJudAcqsU8eRyfzBbcEzGoPWfdM3AGak3cN08WOIw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.14.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@webassemblyjs/ast@1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
||||
@ -2643,10 +2692,10 @@ app-builder-bin@3.7.1:
|
||||
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-3.7.1.tgz#cb0825c5e12efc85b196ac3ed9c89f076c61040e"
|
||||
integrity sha512-ql93vEUq6WsstGXD+SBLSIQw6SNnhbDEM0swzgugytMxLp3rT24Ag/jcC80ZHxiPRTdew1niuR7P3/FCrDqIjw==
|
||||
|
||||
app-builder-lib@22.14.5:
|
||||
version "22.14.5"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-22.14.5.tgz#a61a50b132b858e98fdc70b6b88994ae99b4f96d"
|
||||
integrity sha512-k3VwKP4kpsnUaXoUkm1s4zaSHPHIMFnN4kPMU9yXaKmE1LfHHqBaEah5bXeTAX5V/BC41wFdg8CF5vOjvgy8Rg==
|
||||
app-builder-lib@22.14.13:
|
||||
version "22.14.13"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-22.14.13.tgz#c1f5b6afc86596357598bb90b69eef06c7c2eeb3"
|
||||
integrity sha512-SufmrtxU+D0Tn948fjEwAOlCN9757UXLkzzTWXMwZKR/5hisvgqeeBepWfphMIE6OkDGz0fbzEhL1P2Pty4XMg==
|
||||
dependencies:
|
||||
"7zip-bin" "~5.1.1"
|
||||
"@develar/schema-utils" "~2.6.5"
|
||||
@ -2654,13 +2703,13 @@ app-builder-lib@22.14.5:
|
||||
"@malept/flatpak-bundler" "^0.4.0"
|
||||
async-exit-hook "^2.0.1"
|
||||
bluebird-lst "^1.0.9"
|
||||
builder-util "22.14.5"
|
||||
builder-util-runtime "8.9.1"
|
||||
builder-util "22.14.13"
|
||||
builder-util-runtime "8.9.2"
|
||||
chromium-pickle-js "^0.2.0"
|
||||
debug "^4.3.2"
|
||||
ejs "^3.1.6"
|
||||
electron-osx-sign "^0.5.0"
|
||||
electron-publish "22.14.5"
|
||||
electron-publish "22.14.13"
|
||||
form-data "^4.0.0"
|
||||
fs-extra "^10.0.0"
|
||||
hosted-git-info "^4.0.2"
|
||||
@ -2847,10 +2896,10 @@ async@^2.6.2:
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
async@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
async@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
|
||||
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
@ -3265,21 +3314,31 @@ builder-util-runtime@8.9.1:
|
||||
debug "^4.3.2"
|
||||
sax "^1.2.4"
|
||||
|
||||
builder-util@22.14.5:
|
||||
version "22.14.5"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-22.14.5.tgz#42a18608d2a566c0846e91266464776c8bfb0cc9"
|
||||
integrity sha512-zqIHDFJwmA7jV7SC9aI+33MWwT2mWoijH+Ol9IntNAwuuRXoS+7XeJwnhLBXOhcDBzXT4kDzHnRk4JKeaygEYA==
|
||||
builder-util-runtime@8.9.2:
|
||||
version "8.9.2"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.9.2.tgz#a9669ae5b5dcabfe411ded26678e7ae997246c28"
|
||||
integrity sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A==
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
sax "^1.2.4"
|
||||
|
||||
builder-util@22.14.13:
|
||||
version "22.14.13"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-22.14.13.tgz#41b5b7b4ee53aff4e09cc007fb144522598f3ce6"
|
||||
integrity sha512-oePC/qrrUuerhmH5iaCJzPRAKlSBylrhzuAJmRQClTyWnZUv6jbaHh+VoHMbEiE661wrj2S2aV7/bQh12cj1OA==
|
||||
dependencies:
|
||||
"7zip-bin" "~5.1.1"
|
||||
"@types/debug" "^4.1.6"
|
||||
"@types/fs-extra" "^9.0.11"
|
||||
app-builder-bin "3.7.1"
|
||||
bluebird-lst "^1.0.9"
|
||||
builder-util-runtime "8.9.1"
|
||||
builder-util-runtime "8.9.2"
|
||||
chalk "^4.1.1"
|
||||
cross-spawn "^7.0.3"
|
||||
debug "^4.3.2"
|
||||
fs-extra "^10.0.0"
|
||||
http-proxy-agent "^5.0.0"
|
||||
https-proxy-agent "^5.0.0"
|
||||
is-ci "^3.0.0"
|
||||
js-yaml "^4.1.0"
|
||||
source-map-support "^0.5.19"
|
||||
@ -3820,7 +3879,7 @@ colors@1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
||||
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
|
||||
|
||||
colors@^1.1.2, colors@^1.2.1, colors@^1.4.0:
|
||||
colors@^1.1.2, colors@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
|
||||
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
|
||||
@ -4174,18 +4233,18 @@ css-box-model@^1.2.0:
|
||||
dependencies:
|
||||
tiny-invariant "^1.0.6"
|
||||
|
||||
css-loader@^6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1"
|
||||
integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==
|
||||
css-loader@^6.7.1:
|
||||
version "6.7.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e"
|
||||
integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==
|
||||
dependencies:
|
||||
icss-utils "^5.1.0"
|
||||
postcss "^8.2.15"
|
||||
postcss "^8.4.7"
|
||||
postcss-modules-extract-imports "^3.0.0"
|
||||
postcss-modules-local-by-default "^4.0.0"
|
||||
postcss-modules-scope "^3.0.0"
|
||||
postcss-modules-values "^4.0.0"
|
||||
postcss-value-parser "^4.1.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
semver "^7.3.5"
|
||||
|
||||
css-parse@~2.0.0:
|
||||
@ -4606,14 +4665,14 @@ dlv@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
|
||||
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
|
||||
|
||||
dmg-builder@22.14.5:
|
||||
version "22.14.5"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.14.5.tgz#137c0b55e639badcc0b119eb060e6fa4ed61d948"
|
||||
integrity sha512-1GvFGQE332bvPamcMwZDqWqfWfJTyyDLOsHMcGi0zs+Jh7JOn6/zuBkHJIWHdsj2QJbhzLVyd2/ZqttOKv7I8w==
|
||||
dmg-builder@22.14.13:
|
||||
version "22.14.13"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.14.13.tgz#cc613f3c18e889b8777d525991fd52f50a564f8c"
|
||||
integrity sha512-xNOugB6AbIRETeU2uID15sUfjdZZcKdxK8xkFnwIggsM00PJ12JxpLNPTjcRoUnfwj3WrPjilrO64vRMwNItQg==
|
||||
dependencies:
|
||||
app-builder-lib "22.14.5"
|
||||
builder-util "22.14.5"
|
||||
builder-util-runtime "8.9.1"
|
||||
app-builder-lib "22.14.13"
|
||||
builder-util "22.14.13"
|
||||
builder-util-runtime "8.9.2"
|
||||
fs-extra "^10.0.0"
|
||||
iconv-lite "^0.6.2"
|
||||
js-yaml "^4.1.0"
|
||||
@ -4818,17 +4877,17 @@ ejs@^3.1.6:
|
||||
dependencies:
|
||||
jake "^10.6.1"
|
||||
|
||||
electron-builder@^22.14.5:
|
||||
version "22.14.5"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-22.14.5.tgz#3a25547bd4fe3728d4704da80956a794c5c31496"
|
||||
integrity sha512-N73hSbXFz6Mz5Z6h6C5ly6CB+dUN6k1LuCDJjI8VF47bMXv/QE0HE+Kkb0GPKqTqM7Hsk/yIYX+kHCfSkR5FGg==
|
||||
electron-builder@^22.14.13:
|
||||
version "22.14.13"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-22.14.13.tgz#fd40564685cf5422a8f8d667940af3d3776f4fb8"
|
||||
integrity sha512-3fgLxqF2TXVKiUPeg74O4V3l0l3j7ERLazo8sUbRkApw0+4iVAf2BJkHsHMaXiigsgCoEzK/F4/rB5rne/VAnw==
|
||||
dependencies:
|
||||
"@types/yargs" "^17.0.1"
|
||||
app-builder-lib "22.14.5"
|
||||
builder-util "22.14.5"
|
||||
builder-util-runtime "8.9.1"
|
||||
app-builder-lib "22.14.13"
|
||||
builder-util "22.14.13"
|
||||
builder-util-runtime "8.9.2"
|
||||
chalk "^4.1.1"
|
||||
dmg-builder "22.14.5"
|
||||
dmg-builder "22.14.13"
|
||||
fs-extra "^10.0.0"
|
||||
is-ci "^3.0.0"
|
||||
lazy-val "^1.0.5"
|
||||
@ -4866,14 +4925,14 @@ electron-osx-sign@^0.5.0:
|
||||
minimist "^1.2.0"
|
||||
plist "^3.0.1"
|
||||
|
||||
electron-publish@22.14.5:
|
||||
version "22.14.5"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-22.14.5.tgz#34bcdce671f0e651330db20040d6919c77c94bd6"
|
||||
integrity sha512-h+NANRdaA0PqGF15GKvorseWPzh1PXa/zx4I37//PIokW8eKIov8ky23foUSb55ZFWUHGpxQJux7y2NCfBtQeg==
|
||||
electron-publish@22.14.13:
|
||||
version "22.14.13"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-22.14.13.tgz#8b71e6975af8cc6ac5b21f293ade23f8704047c7"
|
||||
integrity sha512-0oP3QiNj3e8ewOaEpEJV/o6Zrmy2VarVvZ/bH7kyO/S/aJf9x8vQsKVWpsdmSiZ5DJEHgarFIXrnO0ZQf0P9iQ==
|
||||
dependencies:
|
||||
"@types/fs-extra" "^9.0.11"
|
||||
builder-util "22.14.5"
|
||||
builder-util-runtime "8.9.1"
|
||||
builder-util "22.14.13"
|
||||
builder-util-runtime "8.9.2"
|
||||
chalk "^4.1.1"
|
||||
fs-extra "^10.0.0"
|
||||
lazy-val "^1.0.5"
|
||||
@ -5852,11 +5911,6 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fast-safe-stringify@^2.0.4:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
|
||||
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
|
||||
|
||||
fastest-levenshtein@^1.0.12:
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
|
||||
@ -6827,6 +6881,15 @@ http-proxy-agent@^4.0.1:
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
http-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
|
||||
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
|
||||
dependencies:
|
||||
"@tootallnate/once" "2"
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
http-proxy-middleware@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.2.tgz#94d7593790aad6b3de48164f13792262f656c332"
|
||||
@ -6973,12 +7036,7 @@ ignore@^4.0.6:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
ignore@^5.1.4, ignore@^5.1.8:
|
||||
version "5.1.8"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
|
||||
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
|
||||
|
||||
ignore@^5.2.0:
|
||||
ignore@^5.1.4, ignore@^5.1.8, ignore@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
|
||||
@ -8822,15 +8880,15 @@ lodash@4.x, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15,
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
logform@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2"
|
||||
integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==
|
||||
logform@^2.2.0, logform@^2.3.2, logform@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe"
|
||||
integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==
|
||||
dependencies:
|
||||
colors "^1.2.1"
|
||||
fast-safe-stringify "^2.0.4"
|
||||
"@colors/colors" "1.5.0"
|
||||
fecha "^4.2.0"
|
||||
ms "^2.1.1"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
triple-beam "^1.3.0"
|
||||
|
||||
loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
||||
@ -9042,6 +9100,11 @@ merge2@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
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:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
@ -9285,6 +9348,13 @@ mock-fs@^5.1.2:
|
||||
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7"
|
||||
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:
|
||||
version "0.5.34"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
|
||||
@ -9366,10 +9436,10 @@ nan@^2.14.0:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
|
||||
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
|
||||
|
||||
nanoid@^3.1.30:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
|
||||
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
|
||||
nanoid@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
|
||||
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
@ -9412,12 +9482,7 @@ negotiator@0.6.2:
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||
|
||||
neo-async@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
||||
|
||||
neo-async@^2.6.2:
|
||||
neo-async@^2.6.0, neo-async@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
@ -10692,12 +10757,7 @@ postcss-value-parser@^3.3.0:
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
||||
|
||||
postcss-value-parser@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
||||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||
|
||||
postcss-value-parser@^4.2.0:
|
||||
postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
@ -10711,14 +10771,14 @@ postcss@^6.0.14, postcss@^6.0.2:
|
||||
source-map "^0.6.1"
|
||||
supports-color "^5.4.0"
|
||||
|
||||
postcss@^8.1.6, postcss@^8.2.15, postcss@^8.3.0, postcss@^8.4.5:
|
||||
version "8.4.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95"
|
||||
integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==
|
||||
postcss@^8.1.6, postcss@^8.3.0, postcss@^8.4.5, postcss@^8.4.7:
|
||||
version "8.4.8"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032"
|
||||
integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==
|
||||
dependencies:
|
||||
nanoid "^3.1.30"
|
||||
nanoid "^3.3.1"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.1"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
prebuild-install@^7.0.0:
|
||||
version "7.0.0"
|
||||
@ -11120,10 +11180,10 @@ react-router@5.2.1, react-router@^5.2.0:
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-select-event@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-select-event/-/react-select-event-5.1.0.tgz#d45ef68f2a9c872903e8c9725f3ae6e7576f7be0"
|
||||
integrity sha512-D5DzJlYCdZsGbDVFMQFynrG0OLalJM3ZzDT7KQADNVWE604JCeQF9bIuvPZqVD7IzhnPsFzOUCsilzDA6w6WRQ==
|
||||
react-select-event@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-select-event/-/react-select-event-5.3.0.tgz#4548fffd615a47176951cbb301ee21a0c60b582a"
|
||||
integrity sha512-Novkl7X9JJKmDV5LyYaKwl0vffWtqPrBa1vuI0v43P/f87mSA7JfdYxU93SFb99RssphVzBSIAbcnbX1w21QIQ==
|
||||
dependencies:
|
||||
"@testing-library/dom" ">=7"
|
||||
|
||||
@ -11276,7 +11336,7 @@ read@1, read@~1.0.1, read@~1.0.7:
|
||||
dependencies:
|
||||
mute-stream "~0.0.4"
|
||||
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6:
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
@ -11703,6 +11763,11 @@ safe-regex@^1.1.0:
|
||||
dependencies:
|
||||
ret "~0.1.10"
|
||||
|
||||
safe-stable-stringify@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73"
|
||||
integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
@ -11730,10 +11795,10 @@ sanitize-filename@^1.6.2, sanitize-filename@^1.6.3:
|
||||
dependencies:
|
||||
truncate-utf8-bytes "^1.0.0"
|
||||
|
||||
sass-loader@^12.4.0:
|
||||
version "12.4.0"
|
||||
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.4.0.tgz#260b0d51a8a373bb8e88efc11f6ba5583fea0bcf"
|
||||
integrity sha512-7xN+8khDIzym1oL9XyS6zP6Ges+Bo2B2xbPrjdMHEYyV3AQYhd/wXeru++3ODHF0zMjYmVadblSKrPrjEkL8mg==
|
||||
sass-loader@^12.6.0:
|
||||
version "12.6.0"
|
||||
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb"
|
||||
integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==
|
||||
dependencies:
|
||||
klona "^2.0.4"
|
||||
neo-async "^2.6.2"
|
||||
@ -12173,11 +12238,16 @@ source-list-map@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1:
|
||||
"source-map-js@>=0.6.2 <2.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
|
||||
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
|
||||
@ -13024,7 +13094,7 @@ tree-kill@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
triple-beam@^1.2.0, triple-beam@^1.3.0:
|
||||
triple-beam@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
|
||||
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
|
||||
@ -13193,10 +13263,10 @@ type-fest@^0.8.1:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||
|
||||
type-fest@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.0.2.tgz#3f9c39982859f385c77c38b7e5f1432b8a3661c6"
|
||||
integrity sha512-a720oz3Kjbp3ll0zkeN9qjRhO7I34MKMhPGQiQJAmaZQZQ1lo+NWThK322f7sXV+kTg9B1Ybt16KgBXWgteT8w==
|
||||
type-fest@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
|
||||
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
|
||||
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
@ -13899,28 +13969,30 @@ winston-transport-browserconsole@^1.0.5:
|
||||
winston "^3.2.1"
|
||||
winston-transport "^4.3.0"
|
||||
|
||||
winston-transport@^4.3.0, winston-transport@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
|
||||
integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
|
||||
winston-transport@^4.3.0, winston-transport@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa"
|
||||
integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==
|
||||
dependencies:
|
||||
readable-stream "^2.3.7"
|
||||
triple-beam "^1.2.0"
|
||||
logform "^2.3.2"
|
||||
readable-stream "^3.6.0"
|
||||
triple-beam "^1.3.0"
|
||||
|
||||
winston@^3.2.1, winston@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
|
||||
integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
|
||||
winston@^3.2.1, winston@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/winston/-/winston-3.6.0.tgz#be32587a099a292b88c49fac6fa529d478d93fb6"
|
||||
integrity sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w==
|
||||
dependencies:
|
||||
"@dabh/diagnostics" "^2.0.2"
|
||||
async "^3.1.0"
|
||||
async "^3.2.3"
|
||||
is-stream "^2.0.0"
|
||||
logform "^2.2.0"
|
||||
logform "^2.4.0"
|
||||
one-time "^1.0.0"
|
||||
readable-stream "^3.4.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
stack-trace "0.0.x"
|
||||
triple-beam "^1.3.0"
|
||||
winston-transport "^4.4.0"
|
||||
winston-transport "^4.5.0"
|
||||
|
||||
word-wrap@^1.2.3, word-wrap@~1.2.3:
|
||||
version "1.2.3"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user