diff --git a/package.json b/package.json index 03820ee3ad..1fd39ce434 100644 --- a/package.json +++ b/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", diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index a8e903af17..bf074f7324 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -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(); diff --git a/src/common/k8s-api/endpoints/metrics.api.ts b/src/common/k8s-api/endpoints/metrics.api.ts index 29a7ac4ee7..88efc166d1 100644 --- a/src/common/k8s-api/endpoints/metrics.api.ts +++ b/src/common/k8s-api/endpoints/metrics.api.ts @@ -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; diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index c8180383e4..4563707d0c 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -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(); diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 58bdafac58..162cf823ee 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -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); diff --git a/src/main/__test__/router.test.ts b/src/main/__test__/router.test.ts deleted file mode 100644 index 134de337a7..0000000000 --- a/src/main/__test__/router.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/main/__test__/static-file-route.test.ts b/src/main/__test__/static-file-route.test.ts new file mode 100644 index 0000000000..bd66b0b08d --- /dev/null +++ b/src/main/__test__/static-file-route.test.ts @@ -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; + + 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; + + 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; + + const result = await handleStaticFileRoute.handler(request); + + expect(result).toEqual({ statusCode: 404 }); + }); +}); diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 80eb89a96b..75c58aa036 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -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(); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index c78b3fc9ac..777fc47892 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -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."); }); } diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index e3de7ab3f7..aac0e8aaca 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -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); diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 3a4bdea723..7f2fbd7281 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -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(); } diff --git a/src/main/router.ts b/src/main/router.ts deleted file mode 100644 index b0b51a1b2f..0000000000 --- a/src/main/router.ts +++ /dev/null @@ -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 { - path?: string; // *-route - namespace?: string; - service?: string; - account?: string; - release?: string; - repo?: string; - chart?: string; -} - -export interface LensApiRequest

{ - path: string; - payload: P; - params: RouteParams; - cluster: Cluster; - response: http.ServerResponse; - query: URLSearchParams; - raw: { - req: http.IncomingMessage; - res: http.ServerResponse; - }; -} - -function getMimeType(filename: string) { - const mimeTypes: Record = { - html: "text/html", - txt: "text/plain", - css: "text/css", - gif: "image/gif", - jpg: "image/jpeg", - png: "image/png", - svg: "image/svg+xml", - js: "application/javascript", - woff2: "font/woff2", - ttf: "font/ttf", - }; - - return mimeTypes[path.extname(filename).slice(1)] || "text/plain"; -} - -interface Dependencies { - routePortForward: (request: LensApiRequest) => Promise; - 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 { - const url = new URL(req.url, "http://localhost"); - const path = url.pathname; - const method = req.method.toLowerCase(); - const matchingRoute = this.router.route(method, path); - const routeFound = !matchingRoute.isBoom; - - if (routeFound) { - const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params }); - - await matchingRoute.route(request); - - return true; - } - - return false; - } - - protected async getRequest(opts: RouterRequestOpts): Promise { - const { req, res, url, cluster, params } = opts; - const { payload } = await Subtext.parse(req, null, { - parse: true, - output: "data", - }); - - return { - cluster, - path: url.pathname, - raw: { - req, res, - }, - response: res, - query: url.searchParams, - payload, - params, - }; - } - - protected static async handleStaticFile({ params, response }: LensApiRequest): Promise { - 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); - } -} diff --git a/src/main/router/parse-request.injectable.ts b/src/main/router/parse-request.injectable.ts new file mode 100644 index 0000000000..ee623769ad --- /dev/null +++ b/src/main/router/parse-request.injectable.ts @@ -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; diff --git a/src/main/router/router-content-types.ts b/src/main/router/router-content-types.ts new file mode 100644 index 0000000000..8271c1ea06 --- /dev/null +++ b/src/main/router/router-content-types.ts @@ -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) => ({ + statusCode: number; + content: any; + headers: Record; + }); +} + +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 = { + 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"), + }, +}; diff --git a/src/main/router/router.injectable.ts b/src/main/router/router.injectable.ts index ce27c27995..620ce4f2c4 100644 --- a/src/main/router/router.injectable.ts +++ b/src/main/router/router.injectable.ts @@ -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>({ + 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), }); }, }); diff --git a/src/main/router/router.test.ts b/src/main/router/router.test.ts new file mode 100644 index 0000000000..96f0587163 --- /dev/null +++ b/src/main/router/router.test.ts @@ -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>; + + 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), + + injectionToken: routeInjectionToken, + }); + + di.register(injectable); + + router = di.inject(routerInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + describe("when navigating to the route", () => { + let actualPromise: Promise; + 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([]); + }); + }); + }); + }); +}); diff --git a/src/main/router/router.ts b/src/main/router/router.ts new file mode 100644 index 0000000000..ca5da23ce1 --- /dev/null +++ b/src/main/router/router.ts @@ -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 { + path?: string; // *-route + namespace?: string; + service?: string; + account?: string; + release?: string; + repo?: string; + chart?: string; +} + +export interface LensApiRequest

{ + path: string; + payload: P; + params: RouteParams; + cluster: Cluster; + 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[], 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 { + 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 { + 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 { + statusCode?: number; + response?: TResult; + error?: any; + contentType?: LensApiResultContentType; + headers?: { [name: string]: string }; + proxy?: httpProxy; +} + +export type RouteHandler = ( + request: LensApiRequest +) => + | Promise> + | Promise + | LensApiResult + | void; + +export interface Route { + path: string; + method: "get" | "post" | "put" | "patch" | "delete"; + handler: RouteHandler; +} + +const handleRoute = (route: Route) => async (request: LensApiRequest, response: http.ServerResponse) => { + let result: LensApiResult | 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(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(); + } + }; diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts deleted file mode 100644 index 958f729228..0000000000 --- a/src/main/routes/helm-route.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/main/routes/helm/charts/get-chart-route.injectable.ts b/src/main/routes/helm/charts/get-chart-route.injectable.ts new file mode 100644 index 0000000000..523be0e1a6 --- /dev/null +++ b/src/main/routes/helm/charts/get-chart-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/charts/get-chart-values-route.injectable.ts b/src/main/routes/helm/charts/get-chart-values-route.injectable.ts new file mode 100644 index 0000000000..4eeeb263ec --- /dev/null +++ b/src/main/routes/helm/charts/get-chart-values-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/charts/list-charts-route.injectable.ts b/src/main/routes/helm/charts/list-charts-route.injectable.ts new file mode 100644 index 0000000000..a1273ca7af --- /dev/null +++ b/src/main/routes/helm/charts/list-charts-route.injectable.ts @@ -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 => ({ + method: "get", + path: `${apiPrefix}/v2/charts`, + + handler: async () => ({ + response: await helmService.listCharts(), + }), + }), + + injectionToken: routeInjectionToken, +}); + +export default listChartsRouteInjectable; diff --git a/src/main/routes/helm/releases/delete-release-route.injectable.ts b/src/main/routes/helm/releases/delete-release-route.injectable.ts new file mode 100644 index 0000000000..6487cc7ce8 --- /dev/null +++ b/src/main/routes/helm/releases/delete-release-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/releases/get-release-history-route.injectable.ts b/src/main/routes/helm/releases/get-release-history-route.injectable.ts new file mode 100644 index 0000000000..5ceadb2695 --- /dev/null +++ b/src/main/routes/helm/releases/get-release-history-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/releases/get-release-route.injectable.ts b/src/main/routes/helm/releases/get-release-route.injectable.ts new file mode 100644 index 0000000000..e11ab64466 --- /dev/null +++ b/src/main/routes/helm/releases/get-release-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/releases/get-release-values-route.injectable.ts b/src/main/routes/helm/releases/get-release-values-route.injectable.ts new file mode 100644 index 0000000000..92227042e7 --- /dev/null +++ b/src/main/routes/helm/releases/get-release-values-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/releases/install-chart-route.injectable.ts b/src/main/routes/helm/releases/install-chart-route.injectable.ts new file mode 100644 index 0000000000..879e39c4ac --- /dev/null +++ b/src/main/routes/helm/releases/install-chart-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/releases/list-releases-route.injectable.ts b/src/main/routes/helm/releases/list-releases-route.injectable.ts new file mode 100644 index 0000000000..b73c618ff3 --- /dev/null +++ b/src/main/routes/helm/releases/list-releases-route.injectable.ts @@ -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> => ({ + 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; diff --git a/src/main/routes/helm/releases/rollback-release-route.injectable.ts b/src/main/routes/helm/releases/rollback-release-route.injectable.ts new file mode 100644 index 0000000000..1880dc7e1e --- /dev/null +++ b/src/main/routes/helm/releases/rollback-release-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/helm/releases/update-release-route.injectable.ts b/src/main/routes/helm/releases/update-release-route.injectable.ts new file mode 100644 index 0000000000..f97dc1a2ad --- /dev/null +++ b/src/main/routes/helm/releases/update-release-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts deleted file mode 100644 index 635d92b3ba..0000000000 --- a/src/main/routes/index.ts +++ /dev/null @@ -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"; diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts deleted file mode 100644 index 4ed5427d0a..0000000000 --- a/src/main/routes/kubeconfig-route.ts +++ /dev/null @@ -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); - } -} diff --git a/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts b/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts new file mode 100644 index 0000000000..c4eec032fe --- /dev/null +++ b/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts @@ -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> => ({ + 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, + }; +} diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts deleted file mode 100644 index 083801680f..0000000000 --- a/src/main/routes/metrics-route.ts +++ /dev/null @@ -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): Promise { - const queries = promQueries.map(p => p.trim()); - const loaders = new Map>(); - - async function loadMetric(query: string): Promise { - async function loadMetricHelper(): Promise { - 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>(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); - } -} diff --git a/src/main/routes/metrics/add-metrics-route.injectable.ts b/src/main/routes/metrics/add-metrics-route.injectable.ts new file mode 100644 index 0000000000..4dc2158488 --- /dev/null +++ b/src/main/routes/metrics/add-metrics-route.injectable.ts @@ -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): Promise { + const queries = promQueries.map(p => p.trim()); + const loaders = new Map>(); + + async function loadMetric(query: string): Promise { + async function loadMetricHelper(): Promise { + 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>(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 => ({ + method: "post", + path: `${apiPrefix}/metrics`, + handler: addMetricsRoute, + }), + + injectionToken: routeInjectionToken, +}); + +export default addMetricsRouteInjectable; diff --git a/src/main/routes/metrics/get-metric-providers-route.injectable.ts b/src/main/routes/metrics/get-metric-providers-route.injectable.ts new file mode 100644 index 0000000000..8fcba9d984 --- /dev/null +++ b/src/main/routes/metrics/get-metric-providers-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/metrics/metrics-query.ts b/src/main/routes/metrics/metrics-query.ts new file mode 100644 index 0000000000..d6d9532b3b --- /dev/null +++ b/src/main/routes/metrics/metrics-query.ts @@ -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; +}; diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts deleted file mode 100644 index 4d688d68a6..0000000000 --- a/src/main/routes/port-forward-route.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/main/routes/port-forward/create-port-forward.injectable.ts b/src/main/routes/port-forward/functionality/create-port-forward.injectable.ts similarity index 89% rename from src/main/routes/port-forward/create-port-forward.injectable.ts rename to src/main/routes/port-forward/functionality/create-port-forward.injectable.ts index bc00f7d791..1bd7a1ea41 100644 --- a/src/main/routes/port-forward/create-port-forward.injectable.ts +++ b/src/main/routes/port-forward/functionality/create-port-forward.injectable.ts @@ -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", diff --git a/src/main/routes/port-forward/port-forward.ts b/src/main/routes/port-forward/functionality/port-forward.ts similarity index 96% rename from src/main/routes/port-forward/port-forward.ts rename to src/main/routes/port-forward/functionality/port-forward.ts index 964b4e6ff2..dcbfb3d752 100644 --- a/src/main/routes/port-forward/port-forward.ts +++ b/src/main/routes/port-forward/functionality/port-forward.ts @@ -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"; diff --git a/src/main/routes/port-forward/get-current-port-forward-route.injectable.ts b/src/main/routes/port-forward/get-current-port-forward-route.injectable.ts new file mode 100644 index 0000000000..a5b83edbf6 --- /dev/null +++ b/src/main/routes/port-forward/get-current-port-forward-route.injectable.ts @@ -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; diff --git a/src/main/routes/port-forward/route-port-forward/route-port-forward.injectable.ts b/src/main/routes/port-forward/route-port-forward/route-port-forward.injectable.ts deleted file mode 100644 index af7758d0eb..0000000000 --- a/src/main/routes/port-forward/route-port-forward/route-port-forward.injectable.ts +++ /dev/null @@ -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; diff --git a/src/main/routes/port-forward/route-port-forward/route-port-forward.ts b/src/main/routes/port-forward/route-port-forward/route-port-forward.ts deleted file mode 100644 index 267bc7529c..0000000000 --- a/src/main/routes/port-forward/route-port-forward/route-port-forward.ts +++ /dev/null @@ -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, - ); - } - }; diff --git a/src/main/routes/port-forward/start-port-forward-route.injectable.ts b/src/main/routes/port-forward/start-port-forward-route.injectable.ts new file mode 100644 index 0000000000..99f6624cd3 --- /dev/null +++ b/src/main/routes/port-forward/start-port-forward-route.injectable.ts @@ -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; diff --git a/src/main/routes/port-forward/stop-current-port-forward-route.injectable.ts b/src/main/routes/port-forward/stop-current-port-forward-route.injectable.ts new file mode 100644 index 0000000000..b9fce502c1 --- /dev/null +++ b/src/main/routes/port-forward/stop-current-port-forward-route.injectable.ts @@ -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; diff --git a/src/main/routes/resource-applier-route.ts b/src/main/routes/resource-applier-route.ts deleted file mode 100644 index 1072050b9a..0000000000 --- a/src/main/routes/resource-applier-route.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/main/routes/resource-applier/apply-resource-route.injectable.ts b/src/main/routes/resource-applier/apply-resource-route.injectable.ts new file mode 100644 index 0000000000..b1db11cfa3 --- /dev/null +++ b/src/main/routes/resource-applier/apply-resource-route.injectable.ts @@ -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 => ({ + method: "post", + path: `${apiPrefix}/stack`, + + handler: async ({ cluster, payload }) => ({ + response: await new ResourceApplier(cluster).apply(payload), + }), + }), + + injectionToken: routeInjectionToken, +}); + +export default applyResourceRouteInjectable; diff --git a/src/main/routes/resource-applier/patch-resource-route.injectable.ts b/src/main/routes/resource-applier/patch-resource-route.injectable.ts new file mode 100644 index 0000000000..2ac26db726 --- /dev/null +++ b/src/main/routes/resource-applier/patch-resource-route.injectable.ts @@ -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 => ({ + 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; diff --git a/src/main/routes/static-file-route.injectable.ts b/src/main/routes/static-file-route.injectable.ts new file mode 100644 index 0000000000..f35976f320 --- /dev/null +++ b/src/main/routes/static-file-route.injectable.ts @@ -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; +} + +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 => { + 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; diff --git a/src/main/routes/version-route.ts b/src/main/routes/version-route.ts deleted file mode 100644 index 1c5c7c94d7..0000000000 --- a/src/main/routes/version-route.ts +++ /dev/null @@ -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); - } -} diff --git a/src/main/routes/versions/get-version-route.injectable.ts b/src/main/routes/versions/get-version-route.injectable.ts new file mode 100644 index 0000000000..357c09f22d --- /dev/null +++ b/src/main/routes/versions/get-version-route.injectable.ts @@ -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; diff --git a/src/renderer/components/dock/logs/controls.tsx b/src/renderer/components/dock/logs/controls.tsx index 59aead384b..159b135435 100644 --- a/src/renderer/components/dock/logs/controls.tsx +++ b/src/renderer/components/dock/logs/controls.tsx @@ -58,7 +58,7 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen {since && ( Logs from{" "} - {new Date(since[0]).toLocaleString()} + {new Date(since).toLocaleString()} )} diff --git a/src/renderer/components/item-object-list/content.tsx b/src/renderer/components/item-object-list/content.tsx index 69f72cffb7..eb7b5dedb2 100644 --- a/src/renderer/components/item-object-list/content.tsx +++ b/src/renderer/components/item-object-list/content.tsx @@ -68,67 +68,76 @@ export class ItemListLayoutContent 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 ( + onDetails(item)) : undefined} + {...customizeTableRowProps(item)} + > + {isSelectable && ( + 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 ; + } + + return null; + })} + {renderItemMenu && ( + +

+ {renderItemMenu(item, store)} +
+ + )} + + ); + } + @boundMethod getRow(uid: string) { return (
{() => { - 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 ( - onDetails(item)) : undefined} - {...customizeTableRowProps(item)} - > - {isSelectable && ( - 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 ; - } - - return null; - })} - {renderItemMenu && ( - -
- {renderItemMenu(item, store)} -
-
- )} -
- ); + return this.getTableRow(item); }}
@@ -248,6 +257,7 @@ export class ItemListLayoutContent extends React.Component selectable={hasDetailsView} sortable={sortingCallbacks} getTableRow={this.getRow} + renderRow={virtual ? undefined : this.renderRow} items={items} selectedItemId={selectedItemId} noItems={this.renderNoItems()} diff --git a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts index 97f2f40565..4fe1081c02 100644 --- a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts +++ b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts @@ -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}`, diff --git a/yarn.lock b/yarn.lock index 0abca06a20..e2f76b3fa3 100644 --- a/yarn.lock +++ b/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"