Restructure to monorepo (#6907)

* wip: restructure to monorepo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor create-release-pr to a package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* github workflow fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix typo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* add webpack-env types to core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix github workflows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor/fix integration tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* lint fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* yarn run dev

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* eslint settings for vscode

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* move templates to right package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* open-lens build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* integration test fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix nx task dependencies

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use bash shell for unit tests in test workflow

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix test:unit for windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix build:app on windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove ELECTRON_BUILDER_EXTRA_ARGS

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* sync src/ from master

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove Makefile from core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,29 @@
/**
* 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 httpProxy from "http-proxy";
import { webpackDevServerPort } from "../../../../webpack/vars";
import { publicPath } from "../../../common/vars";
import type { LensApiRequest, RouteResponse } from "../../router/route";
const devStaticFileRouteHandlerInjectable = getInjectable({
id: "dev-static-file-route-handler",
instantiate: () => {
const proxy = httpProxy.createProxy();
const proxyTarget = `http://127.0.0.1:${webpackDevServerPort}`;
return async ({ raw: { req, res }}: LensApiRequest<"/{path*}">): Promise<RouteResponse<Buffer>> => {
if (req.url === "/" || !req.url || !req.url.startsWith(publicPath)) {
req.url = `${publicPath}/index.html`;
}
proxy.web(req, res, { target: proxyTarget });
return { proxy };
};
},
});
export default devStaticFileRouteHandlerInjectable;
@@ -0,0 +1,59 @@
/**
* 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 readFileBufferInjectable from "../../../common/fs/read-file-buffer.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import staticFilesDirectoryInjectable from "../../../common/vars/static-files-directory.injectable";
import type { LensApiRequest } from "../../router/route";
import path from "path";
import type { SupportedFileExtension } from "../../router/router-content-types";
import { contentTypes } from "../../router/router-content-types";
import loggerInjectable from "../../../common/logger.injectable";
import { publicPath } from "../../../common/vars";
const prodStaticFileRouteHandlerInjectable = getInjectable({
id: "prod-static-file-route-handler",
instantiate: (di) => {
const readFileBuffer = di.inject(readFileBufferInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const logger = di.inject(loggerInjectable);
return async ({ params }: LensApiRequest<"/{path*}">) => {
let filePath = params.path;
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
const assetFilePath = joinPaths(staticFilesDirectory, filePath);
if (!assetFilePath.startsWith(staticFilesDirectory)) {
return { statusCode: 404 };
}
try {
const fileExtension = path
.extname(assetFilePath)
.slice(1) as SupportedFileExtension;
const contentType = contentTypes[fileExtension] || contentTypes.txt;
return { response: await readFileBuffer(assetFilePath), contentType };
} catch (err) {
if (retryCount > 5) {
logger.error("handleStaticFile:", String(err));
return { statusCode: 404 };
}
filePath = `${publicPath}/index.html`;
}
}
return { statusCode: 404 };
};
},
});
export default prodStaticFileRouteHandlerInjectable;
@@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../router/router.injectable";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
import { route } from "../../router/route";
import prodStaticFileRouteHandlerInjectable from "./production.injectable";
import devStaticFileRouteHandlerInjectable from "./development.injectable";
const staticFileRouteInjectable = getRouteInjectable({
id: "static-file-route",
instantiate: (di) => {
const isDevelopment = di.inject(isDevelopmentInjectable);
return route({
method: "get",
path: `/{path*}`,
})(
isDevelopment
? di.inject(devStaticFileRouteHandlerInjectable)
: di.inject(prodStaticFileRouteHandlerInjectable),
);
},
});
export default staticFileRouteInjectable;
@@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../../router/router.injectable";
import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import getHelmChartReadmeInjectable from "../../../helm/helm-service/get-helm-chart-readme.injectable";
const getHelmChartReadmeRouteInjectable = getRouteInjectable({
id: "get-helm-chart-readme-route",
instantiate: (di) => {
const getHelmChartReadme = di.inject(getHelmChartReadmeInjectable);
return route({
method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}/readme`,
})(async ({ params, query }) => {
const { repo, chart } = params;
return {
response: await getHelmChartReadme(
repo,
chart,
query.get("version") ?? undefined,
),
};
});
},
});
export default getHelmChartReadmeRouteInjectable;
@@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../../router/router.injectable";
import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import getHelmChartValuesInjectable from "../../../helm/helm-service/get-helm-chart-values.injectable";
const getHelmChartRouteValuesInjectable = getRouteInjectable({
id: "get-helm-chart-values-route",
instantiate: (di) => {
const getHelmChartValues = di.inject(getHelmChartValuesInjectable);
return route({
method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}/values`,
})(async ({ params, query }) => ({
response: await getHelmChartValues(
params.repo,
params.chart,
query.get("version") ?? undefined,
),
}));
},
});
export default getHelmChartRouteValuesInjectable;
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../../router/router.injectable";
import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import getHelmChartVersionsInjectable from "../../../helm/helm-service/get-helm-chart-versions.injectable";
const getHelmChartVersionsRouteInjectable = getRouteInjectable({
id: "get-helm-chart-versions-route",
instantiate: (di) => {
const getHelmChartVersions = di.inject(getHelmChartVersionsInjectable);
return route({
method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}/versions`,
})(async ({ params: { repo, chart }}) => ({
response: await getHelmChartVersions(repo, chart),
}));
},
});
export default getHelmChartVersionsRouteInjectable;
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../../router/router.injectable";
import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route";
import listHelmChartsInjectable from "../../../helm/helm-service/list-helm-charts.injectable";
const listHelmChartsRouteInjectable = getRouteInjectable({
id: "list-helm-charts-route",
instantiate: (di) => {
const listHelmCharts = di.inject(listHelmChartsInjectable);
return route({
method: "get",
path: `${apiPrefix}/v2/charts`,
})(async () => ({
response: await listHelmCharts(),
}));
},
});
export default listHelmChartsRouteInjectable;
@@ -0,0 +1,25 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route";
import deleteClusterHelmReleaseInjectable from "../../../helm/helm-service/delete-helm-release.injectable";
const deleteReleaseRouteInjectable = getRouteInjectable({
id: "delete-release-route",
instantiate: (di) => {
const deleteHelmRelease = di.inject(deleteClusterHelmReleaseInjectable);
return clusterRoute({
method: "delete",
path: `${apiPrefix}/v2/releases/{namespace}/{name}`,
})(async ({ cluster, params }) => ({
response: await deleteHelmRelease(cluster, params),
}));
},
});
export default deleteReleaseRouteInjectable;
@@ -0,0 +1,25 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route";
import getClusterHelmReleaseHistoryInjectable from "../../../helm/helm-service/get-helm-release-history.injectable";
const getReleaseRouteHistoryInjectable = getRouteInjectable({
id: "get-release-history-route",
instantiate: (di) => {
const getHelmReleaseHistory = di.inject(getClusterHelmReleaseHistoryInjectable);
return clusterRoute({
method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{name}/history`,
})(async ({ cluster, params }) => ({
response: await getHelmReleaseHistory(cluster, params),
}));
},
});
export default getReleaseRouteHistoryInjectable;
@@ -0,0 +1,29 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route";
import getHelmReleaseInjectable from "../../../helm/helm-service/get-helm-release.injectable";
const getReleaseRouteInjectable = getRouteInjectable({
id: "get-release-route",
instantiate: (di) => {
const getHelmRelease = di.inject(getHelmReleaseInjectable);
return clusterRoute({
method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
})(async ({ cluster, params }) => ({
response: await getHelmRelease(
cluster,
params.release,
params.namespace,
),
}));
},
});
export default getReleaseRouteInjectable;
@@ -0,0 +1,33 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import { getBoolean } from "../../../utils/parse-query";
import { contentTypes } from "../../../router/router-content-types";
import { clusterRoute } from "../../../router/route";
import getClusterHelmReleaseValuesInjectable from "../../../helm/helm-service/get-helm-release-values.injectable";
const getReleaseRouteValuesInjectable = getRouteInjectable({
id: "get-release-values-route",
instantiate: (di) => {
const getClusterHelmReleaseValues = di.inject(getClusterHelmReleaseValuesInjectable);
return clusterRoute({
method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{name}/values`,
})(async ({ cluster, params: { namespace, name }, query }) => ({
response: await getClusterHelmReleaseValues(cluster, {
name,
namespace,
all: getBoolean(query, "all"),
}),
contentType: contentTypes.txt,
}));
},
});
export default getReleaseRouteValuesInjectable;
@@ -0,0 +1,47 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import Joi from "joi";
import { payloadValidatedClusterRoute } from "../../../router/route";
import type { InstallChartArgs } from "../../../helm/helm-service/install-helm-chart.injectable";
import installClusterHelmChartInjectable from "../../../helm/helm-service/install-helm-chart.injectable";
const installChartArgsValidator = Joi.object<InstallChartArgs, true, InstallChartArgs>({
chart: Joi
.string()
.required(),
values: Joi
.object()
.required()
.unknown(true),
name: Joi
.string(),
namespace: Joi
.string()
.required(),
version: Joi
.string()
.required(),
});
const installChartRouteInjectable = getRouteInjectable({
id: "install-chart-route",
instantiate: (di) => {
const installHelmChart = di.inject(installClusterHelmChartInjectable);
return payloadValidatedClusterRoute({
method: "post",
path: `${apiPrefix}/v2/releases`,
payloadValidator: installChartArgsValidator,
})(async ({ payload, cluster }) => ({
response: await installHelmChart(cluster, payload),
statusCode: 201,
}));
},
});
export default installChartRouteInjectable;
@@ -0,0 +1,25 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route";
import listClusterHelmReleasesInjectable from "../../../helm/helm-service/list-helm-releases.injectable";
const listReleasesRouteInjectable = getRouteInjectable({
id: "list-releases-route",
instantiate: (di) => {
const listHelmReleases = di.inject(listClusterHelmReleasesInjectable);
return clusterRoute({
method: "get",
path: `${apiPrefix}/v2/releases/{namespace?}`,
})(async ({ cluster, params }) => ({
response: await listHelmReleases(cluster, params.namespace),
}));
},
});
export default listReleasesRouteInjectable;
@@ -0,0 +1,37 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import Joi from "joi";
import { payloadValidatedClusterRoute } from "../../../router/route";
import rollbackClusterHelmReleaseInjectable from "../../../helm/helm-service/rollback-helm-release.injectable";
interface RollbackReleasePayload {
revision: number;
}
const rollbackReleasePayloadValidator = Joi.object<RollbackReleasePayload, true, RollbackReleasePayload>({
revision: Joi
.number()
.required(),
});
const rollbackReleaseRouteInjectable = getRouteInjectable({
id: "rollback-release-route",
instantiate: (di) => {
const rollbackRelease = di.inject(rollbackClusterHelmReleaseInjectable);
return payloadValidatedClusterRoute({
method: "put",
path: `${apiPrefix}/v2/releases/{namespace}/{name}/rollback`,
payloadValidator: rollbackReleasePayloadValidator,
})(async ({ cluster, params, payload }) => {
await rollbackRelease(cluster, { ...params, ...payload });
});
},
});
export default rollbackReleaseRouteInjectable;
@@ -0,0 +1,45 @@
/**
* 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 { getRouteInjectable } from "../../../router/router.injectable";
import { payloadValidatedClusterRoute } from "../../../router/route";
import Joi from "joi";
import type { UpdateChartArgs } from "../../../helm/helm-service/update-helm-release.injectable";
import updateHelmReleaseInjectable from "../../../helm/helm-service/update-helm-release.injectable";
const updateChartArgsValidator = Joi.object<UpdateChartArgs, true, UpdateChartArgs>({
chart: Joi
.string()
.required(),
version: Joi
.string()
.required(),
values: Joi
.string()
.required(),
});
const updateReleaseRouteInjectable = getRouteInjectable({
id: "update-release-route",
instantiate: (di) => {
const updateRelease = di.inject(updateHelmReleaseInjectable);
return payloadValidatedClusterRoute({
method: "put",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
payloadValidator: updateChartArgsValidator,
})(async ({ cluster, params, payload }) => ({
response: await updateRelease(
cluster,
params.release,
params.namespace,
payload,
),
}));
},
});
export default updateReleaseRouteInjectable;
@@ -0,0 +1,94 @@
/**
* 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 { getRouteInjectable } from "../../router/router.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import type { V1Secret } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node";
import { clusterRoute } from "../../router/route";
import { dump } from "js-yaml";
const getServiceAccountRouteInjectable = getRouteInjectable({
id: "get-service-account-route",
instantiate: () => clusterRoute({
method: "get",
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
})(async ({ params, cluster }) => {
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?.["kubernetes.io/service-account.name"] === params.account;
});
if (!secret) {
return {
error: "No secret found",
statusCode: 404,
};
}
const kubeconfig = generateKubeConfig(params.account, secret, cluster);
if (!kubeconfig) {
return {
error: "No secret found",
statusCode: 404,
};
}
return {
response: kubeconfig,
};
}),
});
export default getServiceAccountRouteInjectable;
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster): string | undefined {
if (!secret.data || !secret.metadata) {
return undefined;
}
const { token, "ca.crt": caCrt } = secret.data;
const tokenData = Buffer.from(token, "base64");
return dump({
"apiVersion": "v1",
"kind": "Config",
"clusters": [
{
"name": cluster.contextName,
"cluster": {
"server": cluster.apiUrl,
"certificate-authority-data": caCrt,
},
},
],
"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,
});
}
@@ -0,0 +1,124 @@
/**
* 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 { getRouteInjectable } from "../../router/router.injectable";
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types";
import { ClusterMetadataKey } from "../../../common/cluster-types";
import type { Cluster } from "../../../common/cluster/cluster";
import { clusterRoute } from "../../router/route";
import { isObject } from "lodash";
import { isRequestError, object } from "../../../common/utils";
import type { GetMetrics } from "../../get-metrics.injectable";
import getMetricsInjectable from "../../get-metrics.injectable";
import loggerInjectable from "../../../common/logger.injectable";
// This is used for backoff retry tracking.
const ATTEMPTS = [false, false, false, false, true];
const loadMetricsFor = (getMetrics: GetMetrics) => async (promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Partial<Record<string, string>>): Promise<any[]> => {
const queries = promQueries.map(p => p.trim());
const loaders = new Map<string, Promise<any>>();
async function loadMetric(query: string): Promise<any> {
async function loadMetricHelper(): Promise<any> {
for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry
try {
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
} catch (error) {
if (
!isRequestError(error)
|| lastAttempt
|| (
!lastAttempt && (
typeof error.statusCode === "number" &&
400 <= error.statusCode && error.statusCode < 500
)
)
) {
throw new Error("Metrics not available", { cause: error });
}
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 addMetricsRouteInjectable = getRouteInjectable({
id: "add-metrics-route",
instantiate: (di) => {
const getMetrics = di.inject(getMetricsInjectable);
const loadMetrics = loadMetricsFor(getMetrics);
const logger = di.inject(loggerInjectable);
return clusterRoute({
method: "post",
path: `${apiPrefix}/metrics`,
})(async ({ cluster, payload, query }) => {
const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {};
try {
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails();
prometheusMetadata.provider = provider?.kind;
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 };
}
if (isObject(payload)) {
const data = payload as Record<string, Record<string, string>>;
const queries = object.entries(data)
.map(([queryName, queryOpts]) => (
provider.getQuery(queryOpts, queryName)
));
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
const response = object.fromEntries(object.keys(data).map((metricName, i) => [metricName, result[i]]));
prometheusMetadata.success = true;
return { response };
}
return { response: {}};
} 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;
}
});
},
});
export default addMetricsRouteInjectable;
@@ -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 { getRouteInjectable } from "../../router/router.injectable";
import { route } from "../../router/route";
import prometheusProvidersInjectable from "../../prometheus/providers.injectable";
const getMetricProvidersRouteInjectable = getRouteInjectable({
id: "get-metric-providers-route",
instantiate: (di) => {
const prometheusProviders = di.inject(prometheusProvidersInjectable);
return route({
method: "get",
path: `${apiPrefix}/metrics/providers`,
})(() => ({
response: (
prometheusProviders
.get()
.map(({ name, kind: id, isConfigurable }) => ({ name, id, isConfigurable }))
),
}));
},
});
export default getMetricProvidersRouteInjectable;
@@ -0,0 +1,28 @@
/**
* 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 { PortForwardArgs, PortForwardDependencies } from "./port-forward";
import { PortForward } from "./port-forward";
import bundledKubectlInjectable from "../../../kubectl/bundled-kubectl.injectable";
import getPortFromStreamInjectable from "../../../utils/get-port-from-stream.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
export type CreatePortForward = (pathToKubeConfig: string, args: PortForwardArgs) => PortForward;
const createPortForwardInjectable = getInjectable({
id: "create-port-forward",
instantiate: (di): CreatePortForward => {
const dependencies: PortForwardDependencies = {
getKubectlBinPath: di.inject(bundledKubectlInjectable).getPath,
getPortFromStream: di.inject(getPortFromStreamInjectable),
logger: di.inject(loggerInjectable),
};
return (pathToKubeConfig, args) => new PortForward(dependencies, pathToKubeConfig, args);
},
});
export default createPortForwardInjectable;
@@ -0,0 +1,109 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { GetPortFromStream } from "../../../utils/get-port-from-stream.injectable";
import type { ChildProcessWithoutNullStreams } from "child_process";
import { spawn } from "child_process";
import * as tcpPortUsed from "tcp-port-used";
import { TypedRegEx } from "typed-regex";
import type { Logger } from "../../../../common/logger";
const internalPortMatcher = "^forwarding from (?<address>.+) ->";
const internalPortRegex = Object.assign(TypedRegEx(internalPortMatcher, "i"), {
rawMatcher: internalPortMatcher,
});
export interface PortForwardArgs {
clusterId: string;
kind: string;
namespace: string;
name: string;
port: number;
forwardPort: number;
}
export interface PortForwardDependencies {
readonly logger: Logger;
getKubectlBinPath: (bundled: boolean) => Promise<string>;
getPortFromStream: GetPortFromStream;
}
export class PortForward {
public static portForwards: PortForward[] = [];
static getPortforward(forward: PortForwardArgs) {
return PortForward.portForwards.find((pf) => (
pf.clusterId == forward.clusterId &&
pf.kind == forward.kind &&
pf.name == forward.name &&
pf.namespace == forward.namespace &&
pf.port == forward.port
));
}
public process?: ChildProcessWithoutNullStreams;
public clusterId: string;
public kind: string;
public namespace: string;
public name: string;
public port: number;
public forwardPort: number;
constructor(private dependencies: PortForwardDependencies, public pathToKubeConfig: string, args: PortForwardArgs) {
this.clusterId = args.clusterId;
this.kind = args.kind;
this.namespace = args.namespace;
this.name = args.name;
this.port = args.port;
this.forwardPort = args.forwardPort;
}
public async start() {
const kubectlBin = await this.dependencies.getKubectlBinPath(true);
const args = [
"--kubeconfig", this.pathToKubeConfig,
"port-forward",
"-n", this.namespace,
`${this.kind}/${this.name}`,
`${this.forwardPort ?? ""}:${this.port}`,
];
this.process = spawn(kubectlBin, args, {
env: process.env,
});
PortForward.portForwards.push(this);
this.process.on("exit", () => {
const index = PortForward.portForwards.indexOf(this);
if (index > -1) {
PortForward.portForwards.splice(index, 1);
}
});
this.process.stderr.on("data", (data) => {
this.dependencies.logger.debug(`[PORT-FORWARD-ROUTE]: kubectl port-forward process stderr: ${data}`);
});
const internalPort = await this.dependencies.getPortFromStream(this.process.stdout, {
lineRegex: internalPortRegex,
});
try {
await tcpPortUsed.waitUntilUsed(internalPort, 500, 15000);
// make sure this.forwardPort is set to the actual port used (if it was 0 then an available port is found by 'kubectl port-forward')
this.forwardPort = internalPort;
return true;
} catch (error) {
this.process.kill();
return false;
}
}
public async stop() {
this.process?.kill();
}
}
@@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars";
import { PortForward } from "./functionality/port-forward";
import { clusterRoute } from "../../router/route";
const getCurrentPortForwardRouteInjectable = getRouteInjectable({
id: "get-current-port-forward-route",
instantiate: () => clusterRoute({
method: "get",
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
})(async ({ params, query, cluster }) => {
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,
},
};
}),
});
export default getCurrentPortForwardRouteInjectable;
@@ -0,0 +1,92 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars";
import { PortForward } from "./functionality/port-forward";
import createPortForwardInjectable from "./functionality/create-port-forward.injectable";
import { clusterRoute } from "../../router/route";
import loggerInjectable from "../../../common/logger.injectable";
const startPortForwardRouteInjectable = getRouteInjectable({
id: "start-current-port-forward-route",
instantiate: (di) => {
const createPortForward = di.inject(createPortForwardInjectable);
const logger = di.inject(loggerInjectable);
return clusterRoute({
method: "post",
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
})(async ({ params, query, cluster }) => {
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}`,
},
};
}
});
},
});
export default startPortForwardRouteInjectable;
@@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars";
import { PortForward } from "./functionality/port-forward";
import { clusterRoute } from "../../router/route";
import loggerInjectable from "../../../common/logger.injectable";
const stopCurrentPortForwardRouteInjectable = getRouteInjectable({
id: "stop-current-port-forward-route",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return clusterRoute({
method: "delete",
path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}`,
})(async ({ params, query, cluster }) => {
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}`,
},
};
}
});
},
});
export default stopCurrentPortForwardRouteInjectable;
@@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars";
import { payloadValidatedClusterRoute } from "../../router/route";
import Joi from "joi";
import createResourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable";
const createResourceRouteInjectable = getRouteInjectable({
id: "create-resource-route",
instantiate: (di) => {
const createResourceApplier = di.inject(createResourceApplierInjectable);
return payloadValidatedClusterRoute({
method: "post",
path: `${apiPrefix}/stack`,
payloadValidator: Joi.string(),
})(async ({ cluster, payload }) => ({
response: await createResourceApplier(cluster).create(payload),
}));
},
});
export default createResourceRouteInjectable;
@@ -0,0 +1,61 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars";
import { payloadValidatedClusterRoute } from "../../router/route";
import Joi from "joi";
import type { Patch } from "rfc6902";
import createResourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable";
interface PatchResourcePayload {
name: string;
kind: string;
patch: Patch;
ns?: string;
}
const patchResourcePayloadValidator = Joi.object<PatchResourcePayload, true, PatchResourcePayload>({
name: Joi
.string()
.required(),
kind: Joi
.string()
.required(),
ns: Joi
.string()
.optional(),
patch: Joi
.array()
.allow(
Joi.object({
op: Joi
.string()
.required(),
}).unknown(true),
),
});
const patchResourceRouteInjectable = getRouteInjectable({
id: "patch-resource-route",
instantiate: (di) => {
const createResourceApplier = di.inject(createResourceApplierInjectable);
return payloadValidatedClusterRoute({
method: "patch",
path: `${apiPrefix}/stack`,
payloadValidator: patchResourcePayloadValidator,
})(async ({ cluster, payload }) => ({
response: await createResourceApplier(cluster).patch(
payload.name,
payload.kind,
payload.patch,
payload.ns,
),
}));
},
});
export default patchResourceRouteInjectable;
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRouteInjectable } from "../../router/router.injectable";
import { route } from "../../router/route";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
const getVersionRouteInjectable = getRouteInjectable({
id: "get-version-route",
instantiate: (di) => {
const buildVersion = di.inject(buildVersionInjectable);
return route({
method: "get",
path: `/version`,
})(() => ({
response: {
version: buildVersion.get(),
},
}));
},
});
export default getVersionRouteInjectable;