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

Get loading the main page to work

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-12-21 13:05:32 -05:00
parent 03a3adf3bc
commit c615b77ac5
54 changed files with 592 additions and 671 deletions

View File

@ -21,7 +21,7 @@
"debug-build": "concurrently yarn:compile:main yarn:compile:extension-types",
"dev-run": "nodemon --watch ./static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
"dev:main": "yarn run compile:main --watch --progress",
"dev:renderer": "yarn run ts-node webpack/dev-server.ts",
"dev:renderer": "yarn run compile:renderer --watch --progress",
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "yarn run webpack --config webpack/main.ts",
"compile:renderer": "yarn run webpack --config webpack/renderer.ts",
@ -252,8 +252,6 @@
"react-router": "^5.3.4",
"react-virtualized-auto-sizer": "^1.0.7",
"readable-stream": "^3.6.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.9",
"rfc6902": "^5.0.1",
"selfsigned": "^2.1.1",
"semver": "^7.3.8",
@ -320,8 +318,6 @@
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/readable-stream": "^2.3.13",
"@types/request": "^2.48.7",
"@types/request-promise-native": "^1.0.18",
"@types/semver": "^7.3.13",
"@types/sharp": "^0.31.0",
"@types/tar": "^6.1.3",
@ -407,7 +403,6 @@
"typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.75.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.11.1",
"webpack-node-externals": "^3.0.0",
"xterm": "^4.19.0",
"xterm-addon-fit": "^0.5.0"

View File

@ -8,7 +8,7 @@ import Joi from "joi";
/**
* JSON serializable metadata type
*/
export type ClusterMetadata = Record<string, string | number | boolean | object>;
export type ClusterMetadata = Partial<Record<string, string | number | boolean | object>>;
/**
* Metadata for cluster's prometheus settings

View File

@ -11,13 +11,10 @@ import type { Kubectl } from "../../main/kubectl/kubectl";
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
import type { KubeApiResource, KubeApiResourceDescriptor } from "../rbac";
import { formatKubeApiResource } from "../rbac";
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import plimit from "p-limit";
import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
import { disposer, isDefined, isRequestError, toJS } from "../utils";
import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
import type { CreateAuthorizationReview } from "./create-authorization-review.injectable";
import type { ListNamespaces } from "./list-namespaces.injectable";
@ -27,11 +24,14 @@ import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable";
import type { CanListResource, RequestNamespaceListPermissions, RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable";
import type { RequestApiResources } from "../../main/cluster/request-api-resources.injectable";
import type { ClusterMetadataDetector } from "../../main/cluster-detectors/base-cluster-detector";
import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable";
export interface ClusterDependencies {
readonly directoryForKubeConfigs: string;
readonly logger: Logger;
readonly detectorRegistry: DetectorRegistry;
readonly clusterVersionDetector: ClusterMetadataDetector;
detectClusterMetadata: DetectClusterMetadata;
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
createKubectl: (clusterVersion: string) => Kubectl;
@ -39,7 +39,6 @@ export interface ClusterDependencies {
requestApiResources: RequestApiResources;
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor;
createListNamespaces: (config: KubeConfig) => ListNamespaces;
createVersionDetector: (cluster: Cluster) => VersionDetector;
broadcastMessage: BroadcastMessage;
loadConfigfromFile: LoadConfigfromFile;
}
@ -455,7 +454,7 @@ export class Cluster implements ClusterModel {
*/
async refreshMetadata() {
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await this.dependencies.detectorRegistry.detectForCluster(this);
const metadata = await this.dependencies.detectClusterMetadata(this);
const existingMetadata = this.metadata;
this.metadata = Object.assign(existingMetadata, metadata);
@ -470,6 +469,7 @@ export class Cluster implements ClusterModel {
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig);
console.log("before this.isAdmin");
this.isAdmin = await canI({
namespace: "kube-system",
resource: "*",
@ -522,13 +522,13 @@ export class Cluster implements ClusterModel {
protected async getConnectionStatus(): Promise<ClusterStatus> {
try {
const versionDetector = this.dependencies.createVersionDetector(this);
const versionData = await versionDetector.detect();
const versionData = await this.dependencies.clusterVersionDetector.detect(this);
this.metadata.version = versionData.value;
this.metadata.version = versionData?.value;
return ClusterStatus.AccessGranted;
} catch (error) {
console.error(error);
this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
if (isRequestError(error)) {
@ -653,7 +653,7 @@ export class Cluster implements ClusterModel {
const namespaceList = [ctx?.namespace].filter(isDefined);
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
const { response } = error as HttpError & { response: Response };
const { response } = error as HttpError & { response: { body: unknown }};
this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
this.dependencies.broadcastMessage(clusterListNamespaceForbiddenChannel, this.id);

View File

@ -4,6 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { apiKubePrefix } from "../vars";
import { lensClusterIdHeader } from "../vars/auth-header";
import isDevelopmentInjectable from "../vars/is-development.injectable";
import apiBaseInjectable from "./api-base.injectable";
import createKubeJsonApiInjectable from "./create-kube-json-api.injectable";
@ -51,7 +52,7 @@ const createKubeApiForClusterInjectable = getInjectable({
debug: isDevelopment,
}, {
headers: {
"Host": `${cluster.metadata.uid}.localhost:${new URL(apiBase.config.serverAddress).port}`,
[lensClusterIdHeader]: cluster.metadata.uid,
},
},
),

View File

@ -4,8 +4,9 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { apiKubePrefix } from "../vars";
import { lensClusterIdHeader } from "../vars/auth-header";
import isDebuggingInjectable from "../vars/is-debugging.injectable";
import { apiBaseHostHeaderInjectionToken, apiBaseServerAddressInjectionToken } from "./api-base-configs";
import { apiBaseServerAddressInjectionToken } from "./api-base-configs";
import createKubeJsonApiInjectable from "./create-kube-json-api.injectable";
import type { KubeJsonApi } from "./kube-json-api";
@ -25,7 +26,7 @@ const createKubeJsonApiForClusterInjectable = getInjectable({
},
{
headers: {
"Host": `${clusterId}.${di.inject(apiBaseHostHeaderInjectionToken)}`,
[lensClusterIdHeader]: clusterId,
},
},
);

View File

@ -1,30 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getClusterIdFromHost } from "../cluster-id-url-parsing";
describe("getClusterIdFromHost", () => {
const clusterFakeId = "fe540901-0bd6-4f6c-b472-bce1559d7c4a";
it("should return undefined for non cluster frame hosts", () => {
expect(getClusterIdFromHost("localhost:45345")).toBeUndefined();
});
it("should return ClusterId for cluster frame hosts", () => {
expect(getClusterIdFromHost(`${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
});
it("should return ClusterId for cluster frame hosts with additional subdomains", () => {
expect(getClusterIdFromHost(`abc.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
});
});

View File

@ -1,27 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../cluster-types";
/**
* Grab the `ClusterId` out of a host that was generated by `getClusterFrameUrl`, or nothing
* @param host The host section of a URL
* @returns The `ClusterId` part of the host, or `undefined`
*/
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
return subDomains.slice(-2, -1)[0]; // ClusterId or undefined
}
/**
* Get the OpenLens backend routing host for a given `ClusterId`
* @param clusterId The ID to put in front of the current host
* @returns a new URL host section
*/
export function getClusterFrameUrl(clusterId: ClusterId) {
return `//${clusterId}.${location.host}`;
}

View File

@ -6,7 +6,6 @@
export * from "./abort-controller";
export * from "./autobind";
export * from "./camelCase";
export * from "./cluster-id-url-parsing";
export * from "./collection-functions";
export * from "./convertCpu";
export * from "./convertMemory";

View File

@ -7,6 +7,9 @@
* A better typed version of `Object.fromEntries` where the keys are known to
* be a specific subset
*/
export function fromEntries<T>(entries: Iterable<readonly [string, T]>): Partial<Record<string, T>>;
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): Record<Key, T>;
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): Record<Key, T> {
return Object.fromEntries(entries) as Record<Key, T>;
}

View File

@ -130,7 +130,7 @@ export function isFunction(val: unknown): val is (...args: unknown[]) => unknown
/**
* Checks if the value in the second position is non-nullable
*/
export function hasDefinedTupleValue<K, V>(pair: [K, V | undefined | null]): pair is [K, V] {
export function hasDefinedTupleValue<K, V>(pair: readonly [K, V | undefined | null]): pair is [K, V] {
return pair[1] != null;
}

View File

@ -7,3 +7,4 @@
* This is the header name that we use for request authentication
*/
export const lensAuthenticationHeader = "Authorization";
export const lensClusterIdHeader = "Lens-Cluster-Id";

View File

@ -0,0 +1,21 @@
/**
* 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 joinPathsInjectable from "../path/join-paths.injectable";
import appNameInjectable from "./app-name.injectable";
import staticFilesDirectoryInjectable from "./static-files-directory.injectable";
const windowFilePathInjectable = getInjectable({
id: "window-file-path",
instantiate: (di) => {
const joinPaths = di.inject(joinPathsInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const applicationName = di.inject(appNameInjectable);
return joinPaths(staticFilesDirectory, "build", `${applicationName}.html`);
},
});
export default windowFilePathInjectable;

View File

@ -79,7 +79,7 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
toPairs,
map(([key, value]) => [
key,
normalizedParams[key].stringify(value),
normalizedParams[key]?.stringify(value),
]),
fromPairs,
);

View File

@ -3,23 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestPromiseOptions } from "request-promise-native";
import { getInjectionToken } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster";
import type { K8sRequest } from "../k8s-request.injectable";
export interface ClusterDetectionResult {
value: string | number | boolean;
accuracy: number;
}
export abstract class BaseClusterDetector {
abstract readonly key: string;
constructor(public readonly cluster: Cluster, private _k8sRequest: K8sRequest) {}
abstract detect(): Promise<ClusterDetectionResult | null>;
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
return this._k8sRequest(this.cluster, path, options);
}
export interface ClusterMetadataDetector {
readonly key: string;
detect(cluster: Cluster): Promise<ClusterDetectionResult | null>;
}
export const clusterMetadataDetectorInjectionToken = getInjectionToken<ClusterMetadataDetector>({
id: "cluster-metadata-detector-token",
});

View File

@ -0,0 +1,141 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { clusterMetadataDetectorInjectionToken } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { getInjectable } from "@ogre-tools/injectable";
import k8SRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import requestClusterVersionInjectable from "./request-cluster-version.injectable";
const isGKE = (version: string) => version.includes("gke");
const isEKS = (version: string) => version.includes("eks");
const isIKS = (version: string) => version.includes("IKS");
const isAKS = (cluster: Cluster) => cluster.apiUrl.includes("azmk8s.io");
const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-");
const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
const isMinikube = (cluster: Cluster) => cluster.contextName.startsWith("minikube");
const isMicrok8s = (cluster: Cluster) => cluster.contextName.startsWith("microk8s");
const isKind = (cluster: Cluster) => cluster.contextName.startsWith("kubernetes-admin@kind-");
const isDockerDesktop = (cluster: Cluster) => cluster.contextName === "docker-desktop";
const isTke = (version: string) => version.includes("-tke.");
const isCustom = (version: string) => version.includes("+");
const isVMWare = (version: string) => version.includes("+vmware");
const isRke = (version: string) => version.includes("-rancher");
const isRancherDesktop = (cluster: Cluster) => cluster.contextName === "rancher-desktop";
const isK3s = (version: string) => version.includes("+k3s");
const isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s");
const isAlibaba = (version: string) => version.includes("-aliyun");
const isHuawei = (version: string) => version.includes("-CCE");
const clusterDistributionDetectorInjectable = getInjectable({
id: "cluster-distribution-detector",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
const requestClusterVersion = di.inject(requestClusterVersionInjectable);
const isOpenshift = async (cluster: Cluster) => {
try {
const { paths = [] } = await k8sRequest(cluster, "") as { paths?: string[] };
return paths.includes("/apis/project.openshift.io");
} catch (e) {
return false;
}
};
return {
key: ClusterMetadataKey.DISTRIBUTION,
detect: async (cluster) => {
const version = await requestClusterVersion(cluster);
if (isRke(version)) {
return { value: "rke", accuracy: 80 };
}
if (isRancherDesktop(cluster)) {
return { value: "rancher-desktop", accuracy: 80 };
}
if (isK3s(version)) {
return { value: "k3s", accuracy: 80 };
}
if (isGKE(version)) {
return { value: "gke", accuracy: 80 };
}
if (isEKS(version)) {
return { value: "eks", accuracy: 80 };
}
if (isIKS(version)) {
return { value: "iks", accuracy: 80 };
}
if (isAKS(cluster)) {
return { value: "aks", accuracy: 80 };
}
if (isDigitalOcean(cluster)) {
return { value: "digitalocean", accuracy: 90 };
}
if (isK0s(version)) {
return { value: "k0s", accuracy: 80 };
}
if (isVMWare(version)) {
return { value: "vmware", accuracy: 90 };
}
if (isMirantis(version)) {
return { value: "mirantis", accuracy: 90 };
}
if (isAlibaba(version)) {
return { value: "alibaba", accuracy: 90 };
}
if (isHuawei(version)) {
return { value: "huawei", accuracy: 90 };
}
if (isTke(version)) {
return { value: "tencent", accuracy: 90 };
}
if (isMinikube(cluster)) {
return { value: "minikube", accuracy: 80 };
}
if (isMicrok8s(cluster)) {
return { value: "microk8s", accuracy: 80 };
}
if (isKind(cluster)) {
return { value: "kind", accuracy: 70 };
}
if (isDockerDesktop(cluster)) {
return { value: "docker-desktop", accuracy: 80 };
}
if (isCustom(version)) {
if (await isOpenshift(cluster)) {
return { value: "openshift", accuracy: 90 };
}
return { value: "custom", accuracy: 10 };
}
return { value: "unknown", accuracy: 10 };
},
};
},
injectionToken: clusterMetadataDetectorInjectionToken,
});
export default clusterDistributionDetectorInjectable;

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { clusterMetadataDetectorInjectionToken } from "./base-cluster-detector";
import { createHash } from "crypto";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { getInjectable } from "@ogre-tools/injectable";
import k8SRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster";
const clusterIdDetectorFactoryInjectable = getInjectable({
id: "cluster-id-detector-factory",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
const getDefaultNamespaceId = async (cluster: Cluster) => {
const { metadata } = await k8sRequest(cluster, "/api/v1/namespaces/default") as { metadata: { uid: string }};
return metadata.uid;
};
return {
key: ClusterMetadataKey.CLUSTER_ID,
detect: async (cluster) => {
let id: string;
try {
id = await getDefaultNamespaceId(cluster);
} catch(_) {
id = cluster.apiUrl;
}
const value = createHash("sha256").update(id).digest("hex");
return { value, accuracy: 100 };
},
};
},
injectionToken: clusterMetadataDetectorInjectionToken,
});
export default clusterIdDetectorFactoryInjectable;

View File

@ -1,31 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { createHash } from "crypto";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class ClusterIdDetector extends BaseClusterDetector {
key = ClusterMetadataKey.CLUSTER_ID;
public async detect() {
let id: string;
try {
id = await this.getDefaultNamespaceId();
} catch(_) {
id = this.cluster.apiUrl;
}
const value = createHash("sha256").update(id).digest("hex");
return { value, accuracy: 100 };
}
protected async getDefaultNamespaceId() {
const response = await this.k8sRequest("/api/v1/namespaces/default");
return response.metadata.uid;
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { clusterMetadataDetectorInjectionToken } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { getInjectable } from "@ogre-tools/injectable";
import requestClusterVersionInjectable from "./request-cluster-version.injectable";
const clusterLastSeenDetectorInjectable = getInjectable({
id: "cluster-last-seen-detector",
instantiate: (di) => {
const requestClusterVersion = di.inject(requestClusterVersionInjectable);
return {
key: ClusterMetadataKey.LAST_SEEN,
detect: async (cluster) => {
try {
await requestClusterVersion(cluster);
return { value: new Date().toJSON(), accuracy: 100 };
} catch {
return null;
}
},
};
},
injectionToken: clusterMetadataDetectorInjectionToken,
});
export default clusterLastSeenDetectorInjectable;

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { clusterMetadataDetectorInjectionToken } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { getInjectable } from "@ogre-tools/injectable";
import k8SRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster";
const clusterNodeCountDetectorInjectable = getInjectable({
id: "cluster-node-count-detector",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
const requestNodeCount = async (cluster: Cluster) => {
const { items } = await k8sRequest(cluster, "/api/v1/nodes") as { items: unknown[] };
return items.length;
};
return {
key: ClusterMetadataKey.NODES_COUNT,
detect: async (cluster) => {
try {
return {
value: await requestNodeCount(cluster),
accuracy: 1000,
};
} catch {
return null;
}
},
};
},
injectionToken: clusterMetadataDetectorInjectionToken,
});
export default clusterNodeCountDetectorInjectable;

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { clusterMetadataDetectorInjectionToken } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { getInjectable } from "@ogre-tools/injectable";
import requestClusterVersionInjectable from "./request-cluster-version.injectable";
const clusterVersionDetectorInjectable = getInjectable({
id: "cluster-version-detector",
instantiate: (di) => {
const requestClusterVersion = di.inject(requestClusterVersionInjectable);
return {
key: ClusterMetadataKey.VERSION,
detect: async (cluster) => ({
value: await requestClusterVersion(cluster),
accuracy: 100,
}),
};
},
injectionToken: clusterMetadataDetectorInjectionToken,
});
export default clusterVersionDetectorInjectable;

View File

@ -1,21 +0,0 @@
/**
* 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 { VersionDetector } from "./version-detector";
import k8sRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster";
const createVersionDetectorInjectable = getInjectable({
id: "create-version-detector",
instantiate: (di) => {
const k8sRequest = di.inject(k8sRequestInjectable);
return (cluster: Cluster) =>
new VersionDetector(cluster, k8sRequest);
},
});
export default createVersionDetectorInjectable;

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { getInjectable } from "@ogre-tools/injectable";
import { groupBy, reduce } from "lodash";
import { filter, map } from "lodash/fp";
import type { ClusterMetadata } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster";
import { hasDefinedTupleValue, isDefined, object } from "../../common/utils";
import type { ClusterDetectionResult, ClusterMetadataDetector } from "./base-cluster-detector";
import { clusterMetadataDetectorInjectionToken } from "./base-cluster-detector";
export type DetectClusterMetadata = (cluster: Cluster) => Promise<ClusterMetadata>;
const pickHighestAccuracy = (prev: ClusterDetectionResult, curr: ClusterDetectionResult) => (
prev.accuracy > curr.accuracy
? prev
: curr
);
const detectMetadataWithFor = (cluster: Cluster) => async (clusterMetadataDetector: ClusterMetadataDetector) => {
try {
return await clusterMetadataDetector.detect(cluster);
} catch {
return null;
}
};
const detectClusterMetadataInjectable = getInjectable({
id: "detect-cluster-metadata",
instantiate: (di): DetectClusterMetadata => {
const clusterMetadataDetectors = di.injectMany(clusterMetadataDetectorInjectionToken);
return async (cluster) => {
const entries = pipeline(
await Promise.all(clusterMetadataDetectors.map(detectMetadataWithFor(cluster))),
filter(isDefined),
(arg) => groupBy(arg, "key"),
(arg) => object.entries(arg),
map(([ key, results ]) => [key, reduce(results, pickHighestAccuracy)] as const),
filter(hasDefinedTupleValue),
);
return object.fromEntries(entries);
};
},
});
export default detectClusterMetadataInjectable;

View File

@ -1,16 +0,0 @@
/**
* 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 { DetectorRegistry } from "./detector-registry";
import k8sRequestInjectable from "../k8s-request.injectable";
const detectorRegistryInjectable = getInjectable({
id: "detector-registry",
instantiate: (di) =>
new DetectorRegistry({ k8sRequest: di.inject(k8sRequestInjectable) }),
});
export default detectorRegistryInjectable;

View File

@ -1,55 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observable } from "mobx";
import type { ClusterMetadata } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster";
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
import type { K8sRequest } from "../k8s-request.injectable";
interface Dependencies {
k8sRequest: K8sRequest;
}
export type DetectorConstructor = new (cluster: Cluster, k8sRequest: K8sRequest) => BaseClusterDetector;
export class DetectorRegistry {
constructor(private dependencies: Dependencies) {}
registry = observable.array<DetectorConstructor>([], { deep: false });
add(detectorClass: DetectorConstructor): this {
this.registry.push(detectorClass);
return this;
}
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
const results: { [key: string]: ClusterDetectionResult } = {};
for (const detectorClass of this.registry) {
const detector = new detectorClass(cluster, this.dependencies.k8sRequest);
try {
const data = await detector.detect();
if (!data) continue;
const existingValue = results[detector.key];
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
results[detector.key] = data;
} catch (e) {
// detector raised error, do nothing
}
}
const metadata: ClusterMetadata = {};
for (const [key, result] of Object.entries(results)) {
metadata[key] = result.value;
}
return metadata;
}
}

View File

@ -1,189 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class DistributionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.DISTRIBUTION;
public async detect() {
const version = await this.getKubernetesVersion();
if (this.isRke(version)) {
return { value: "rke", accuracy: 80 };
}
if (this.isRancherDesktop()) {
return { value: "rancher-desktop", accuracy: 80 };
}
if (this.isK3s(version)) {
return { value: "k3s", accuracy: 80 };
}
if (this.isGKE(version)) {
return { value: "gke", accuracy: 80 };
}
if (this.isEKS(version)) {
return { value: "eks", accuracy: 80 };
}
if (this.isIKS(version)) {
return { value: "iks", accuracy: 80 };
}
if (this.isAKS()) {
return { value: "aks", accuracy: 80 };
}
if (this.isDigitalOcean()) {
return { value: "digitalocean", accuracy: 90 };
}
if (this.isK0s(version)) {
return { value: "k0s", accuracy: 80 };
}
if (this.isVMWare(version)) {
return { value: "vmware", accuracy: 90 };
}
if (this.isMirantis(version)) {
return { value: "mirantis", accuracy: 90 };
}
if (this.isAlibaba(version)) {
return { value: "alibaba", accuracy: 90 };
}
if (this.isHuawei(version)) {
return { value: "huawei", accuracy: 90 };
}
if (this.isTke(version)) {
return { value: "tencent", accuracy: 90 };
}
if (this.isMinikube()) {
return { value: "minikube", accuracy: 80 };
}
if (this.isMicrok8s()) {
return { value: "microk8s", accuracy: 80 };
}
if (this.isKind()) {
return { value: "kind", accuracy: 70 };
}
if (this.isDockerDesktop()) {
return { value: "docker-desktop", accuracy: 80 };
}
if (this.isCustom(version) && await this.isOpenshift()) {
return { value: "openshift", accuracy: 90 };
}
if (this.isCustom(version)) {
return { value: "custom", accuracy: 10 };
}
return { value: "unknown", accuracy: 10 };
}
public async getKubernetesVersion() {
const response = await this.k8sRequest("/version");
return response.gitVersion;
}
protected isGKE(version: string) {
return version.includes("gke");
}
protected isEKS(version: string) {
return version.includes("eks");
}
protected isIKS(version: string) {
return version.includes("IKS");
}
protected isAKS() {
return this.cluster.apiUrl.includes("azmk8s.io");
}
protected isMirantis(version: string) {
return version.includes("-mirantis-") || version.includes("-docker-");
}
protected isDigitalOcean() {
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
}
protected isMinikube() {
return this.cluster.contextName.startsWith("minikube");
}
protected isMicrok8s() {
return this.cluster.contextName.startsWith("microk8s");
}
protected isKind() {
return this.cluster.contextName.startsWith("kubernetes-admin@kind-");
}
protected isDockerDesktop() {
return this.cluster.contextName === "docker-desktop";
}
protected isTke(version: string) {
return version.includes("-tke.");
}
protected isCustom(version: string) {
return version.includes("+");
}
protected isVMWare(version: string) {
return version.includes("+vmware");
}
protected isRke(version: string) {
return version.includes("-rancher");
}
protected isRancherDesktop() {
return this.cluster.contextName === "rancher-desktop";
}
protected isK3s(version: string) {
return version.includes("+k3s");
}
protected isK0s(version: string) {
return version.includes("-k0s") || version.includes("+k0s");
}
protected isAlibaba(version: string) {
return version.includes("-aliyun");
}
protected isHuawei(version: string) {
return version.includes("-CCE");
}
protected async isOpenshift() {
try {
const response = await this.k8sRequest("");
return response.paths?.includes("/apis/project.openshift.io");
} catch (e) {
return false;
}
}
}

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class LastSeenDetector extends BaseClusterDetector {
key = ClusterMetadataKey.LAST_SEEN;
public async detect() {
if (!this.cluster.accessible) return null;
await this.k8sRequest("/version");
return { value: new Date().toJSON(), accuracy: 100 };
}
}

View File

@ -1,24 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class NodesCountDetector extends BaseClusterDetector {
key = ClusterMetadataKey.NODES_COUNT;
public async detect() {
if (!this.cluster.accessible) return null;
const nodeCount = await this.getNodeCount();
return { value: nodeCount, accuracy: 100 };
}
protected async getNodeCount(): Promise<number> {
const response = await this.k8sRequest("/api/v1/nodes");
return response.items.length;
}
}

View File

@ -0,0 +1,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 type { Cluster } from "../../common/cluster/cluster";
import k8SRequestInjectable from "../k8s-request.injectable";
const requestClusterVersionInjectable = getInjectable({
id: "request-cluster-version",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
return async (cluster: Cluster) => {
const { gitVersion } = await k8sRequest(cluster, "/version") as { gitVersion: string };
return gitVersion;
};
},
});
export default requestClusterVersionInjectable;

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class VersionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.VERSION;
public async detect() {
const version = await this.getKubernetesVersion();
return { value: version, accuracy: 100 };
}
public async getKubernetesVersion() {
const response = await this.k8sRequest("/version");
return response.gitVersion;
}
}

View File

@ -4,13 +4,11 @@
*/
import "../../common/ipc/cluster";
import type http from "http";
import type { IObservableValue, ObservableSet } from "mobx";
import { action, makeObservable, observe, reaction, toJS } from "mobx";
import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger";
import { apiKubePrefix } from "../../common/vars";
import { getClusterIdFromHost, isErrnoException } from "../../common/utils";
import { isErrnoException } from "../../common/utils";
import type { KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities/kubernetes-cluster";
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../../common/ipc";
@ -259,27 +257,6 @@ export class ClusterManager {
cluster.disconnect();
});
}
getClusterForRequest = (req: http.IncomingMessage): Cluster | undefined => {
if (!req.headers.host) {
return undefined;
}
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.url && req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
const cluster = this.dependencies.store.getById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
return cluster;
}
return this.dependencies.store.getById(getClusterIdFromHost(req.headers.host));
};
}
export function catalogEntityFromCluster(cluster: Cluster) {

View File

@ -14,11 +14,11 @@ import createAuthorizationReviewInjectable from "../../common/cluster/create-aut
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createListApiResourcesInjectable from "../cluster/request-api-resources.injectable";
import loggerInjectable from "../../common/logger.injectable";
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable";
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
const createClusterInjectable = getInjectable({
id: "create-cluster",
@ -34,10 +34,10 @@ const createClusterInjectable = getInjectable({
requestApiResources: di.inject(createListApiResourcesInjectable),
createListNamespaces: di.inject(listNamespacesInjectable),
logger: di.inject(loggerInjectable),
detectorRegistry: di.inject(detectorRegistryInjectable),
createVersionDetector: di.inject(createVersionDetectorInjectable),
broadcastMessage: di.inject(broadcastMessageInjectable),
loadConfigfromFile: di.inject(loadConfigfromFileInjectable),
clusterVersionDetector: di.inject(clusterVersionDetectorInjectable),
detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
};
return (model, configData) => new Cluster(dependencies, model, configData);

View File

@ -4,7 +4,9 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../common/cluster/cluster";
import nodeFetchModuleInjectable from "../common/fetch/fetch-module.injectable";
import type { RequestMetricsParams } from "../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
import { object } from "../common/utils";
import k8sRequestInjectable from "./k8s-request.injectable";
export type GetMetrics = (cluster: Cluster, prometheusPath: string, queryParams: RequestMetricsParams & { query: string }) => Promise<unknown>;
@ -14,6 +16,7 @@ const getMetricsInjectable = getInjectable({
instantiate: (di): GetMetrics => {
const k8sRequest = di.inject(k8sRequestInjectable);
const { FormData } = di.inject(nodeFetchModuleInjectable);
return async (
cluster,
@ -22,13 +25,15 @@ const getMetricsInjectable = getInjectable({
) => {
const prometheusPrefix = cluster.preferences.prometheus?.prefix || "";
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
const body = new FormData();
for (const [key, value] of object.entries(queryParams)) {
body.set(key, `${value}`);
}
return k8sRequest(cluster, metricsPath, {
timeout: 0,
resolveWithFullResponse: false,
json: true,
method: "POST",
form: queryParams,
body,
});
};
},

View File

@ -2,38 +2,45 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestPromiseOptions } from "request-promise-native";
import request from "request-promise-native";
import { apiKubePrefix } from "../common/vars";
import type { Cluster } from "../common/cluster/cluster";
import { getInjectable } from "@ogre-tools/injectable";
import lensProxyPortInjectable from "./lens-proxy/lens-proxy-port.injectable";
import { lensAuthenticationHeaderValueInjectionToken } from "../common/auth/header-value";
import { lensAuthenticationHeader } from "../common/vars/auth-header";
import type { AuthenticatedRequestInit } from "../common/fetch/lens-authed-fetch.injectable";
import lensAuthenticatedFetchInjectable from "../common/fetch/lens-authed-fetch.injectable";
import nodeFetchModuleInjectable from "../common/fetch/fetch-module.injectable";
import { lensClusterIdHeader } from "../common/vars/auth-header";
export type K8sRequest = (cluster: Cluster, path: string, options?: RequestPromiseOptions) => Promise<any>;
export type K8sRequest = (cluster: Cluster, path: string, options?: AuthenticatedRequestInit) => Promise<unknown>;
const k8sRequestInjectable = getInjectable({
id: "k8s-request",
instantiate: (di) => {
instantiate: (di): K8sRequest => {
const lensProxyPort = di.inject(lensProxyPortInjectable);
const lensAuthenticationHeaderValue = di.inject(lensAuthenticationHeaderValueInjectionToken);
const fetch = di.inject(lensAuthenticatedFetchInjectable);
const { Headers } = di.inject(nodeFetchModuleInjectable);
return async (
cluster: Cluster,
path: string,
options: RequestPromiseOptions = {},
) => {
return async (cluster, path, init = {}) => {
const kubeProxyUrl = `https://localhost:${lensProxyPort.get()}${apiKubePrefix}`;
const headers = new Headers(init.headers);
options.headers ??= {};
options.json ??= true;
options.timeout ??= 30000;
options.headers.Host = `${cluster.id}.${new URL(kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
options.headers[lensAuthenticationHeader] = lensAuthenticationHeaderValue;
headers.set(lensClusterIdHeader, cluster.id);
return request(kubeProxyUrl + path, options);
const response = await fetch(kubeProxyUrl + path, {
...init,
headers,
});
if (200 <= response.status && response.status < 300) {
const body = await response.text();
console.log(body);
return JSON.parse(body);
}
throw new Error(`${(init.method ?? "GET").toUpperCase()} ${path} failed: ${response.statusText}`);
};
},
});

View File

@ -0,0 +1,46 @@
/**
* 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 { IncomingMessage } from "http";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import { apiKubePrefix } from "../../common/vars";
import { lensClusterIdHeader } from "../../common/vars/auth-header";
export type GetClusterForRequest = (req: IncomingMessage) => Cluster | undefined;
const getClusterForRequestInjectable = getInjectable({
id: "get-cluster-for-request",
instantiate: (di): GetClusterForRequest => {
const getClusterById = di.inject(getClusterByIdInjectable);
return (req) => {
const clusterId = req.headers[lensClusterIdHeader.toLowerCase()];
console.log(clusterId);
if (typeof clusterId === "string") {
return getClusterById(clusterId);
}
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.url && req.headers.host?.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
const cluster = getClusterById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
return cluster;
}
return undefined;
};
},
});
export default getClusterForRequestInjectable;

View File

@ -7,7 +7,6 @@ import { LensProxy } from "./lens-proxy";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import routeRequestInjectable from "../router/route-request.injectable";
import httpProxy from "http-proxy";
import clusterManagerInjectable from "../cluster/manager.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
import lensProxyPortInjectable from "./lens-proxy-port.injectable";
import contentSecurityPolicyInjectable from "../../common/vars/content-security-policy.injectable";
@ -15,6 +14,7 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.inject
import loggerInjectable from "../../common/logger.injectable";
import authHeaderValueInjectable from "./auth-header-value.injectable";
import lensProxyCertificateInjectable from "./certificate.injectable";
import getClusterForRequestInjectable from "./get-cluster-for-request.injectable";
const lensProxyInjectable = getInjectable({
id: "lens-proxy",
@ -24,7 +24,7 @@ const lensProxyInjectable = getInjectable({
proxy: httpProxy.createProxy(),
kubeApiUpgradeRequest,
shellApiRequest: di.inject(shellApiRequestInjectable),
getClusterForRequest: di.inject(clusterManagerInjectable).getClusterForRequest,
getClusterForRequest: di.inject(getClusterForRequestInjectable),
lensProxyPort: di.inject(lensProxyPortInjectable),
contentSecurityPolicy: di.inject(contentSecurityPolicyInjectable),
emitAppEvent: di.inject(emitAppEventInjectable),

View File

@ -9,7 +9,6 @@ import https from "https";
import type httpProxy from "http-proxy";
import { apiPrefix, apiKubePrefix } from "../../common/vars";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import type { Cluster } from "../../common/cluster/cluster";
import type { ProxyApiRequestArgs } from "./proxy-functions";
import { getBoolean } from "../utils/parse-query";
import assert from "assert";
@ -22,15 +21,15 @@ import { contentTypes } from "../router/router-content-types";
import { writeServerResponseFor } from "../router/write-server-response";
import { URL } from "url";
import type { SelfSignedCert } from "selfsigned";
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
import type { GetClusterForRequest } from "./get-cluster-for-request.injectable";
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
export type ProxyApiRequest = (args: ProxyApiRequestArgs) => void | Promise<void>;
interface Dependencies {
getClusterForRequest: GetClusterForRequest;
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
shellApiRequest: ProxyApiRequest;
kubeApiUpgradeRequest: ProxyApiRequest;
emitAppEvent: EmitAppEvent;
routeRequest: RouteRequest;
readonly proxy: httpProxy;
@ -260,6 +259,9 @@ export class LensProxy {
protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) {
const cluster = this.dependencies.getClusterForRequest(req);
console.log(cluster?.id, req.url, req.headers);
const writeServerResponse = writeServerResponseFor(res);
if (cluster) {

View File

@ -7,11 +7,11 @@ import { chunk } from "lodash";
import type { ConnectionOptions } from "tls";
import { connect } from "tls";
import url from "url";
import type { ProxyApiRequestArgs } from "./types";
import type { ProxyApiRequest } from "../lens-proxy";
const skipRawHeaders = new Set(["Host", "Authorization"]);
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
export const kubeApiUpgradeRequest: ProxyApiRequest = async ({ req, socket, head, cluster }) => {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url;
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
const apiUrl = url.parse(cluster.apiUrl);
@ -62,4 +62,4 @@ export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: Prox
socket.on("error", function () {
proxySocket.end();
});
}
};

View File

@ -3,19 +3,41 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shellApiRequest } from "./shell-api-request";
import shellRequestAuthenticatorInjectable from "./shell-request-authenticator/shell-request-authenticator.injectable";
import clusterManagerInjectable from "../../../cluster/manager.injectable";
import openShellSessionInjectable from "../../../shell-session/create-shell-session.injectable";
import type { ProxyApiRequest } from "../../lens-proxy";
import getClusterForRequestInjectable from "../../get-cluster-for-request.injectable";
import URLParse from "url-parse";
import { Server as WebSocketServer } from "ws";
import loggerInjectable from "../../../../common/logger.injectable";
const shellApiRequestInjectable = getInjectable({
id: "shell-api-request",
instantiate: (di) => shellApiRequest({
openShellSession: di.inject(openShellSessionInjectable),
authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate,
clusterManager: di.inject(clusterManagerInjectable),
}),
instantiate: (di): ProxyApiRequest => {
const openShellSession = di.inject(openShellSessionInjectable);
const authenticateRequest = di.inject(shellRequestAuthenticatorInjectable).authenticate;
const getClusterForRequest = di.inject(getClusterForRequestInjectable);
const logger = di.inject(loggerInjectable);
return ({ req, socket, head }) => {
const cluster = getClusterForRequest(req);
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
if (!tabId || !cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
socket.write("Invalid shell request");
socket.end();
} else {
const ws = new WebSocketServer({ noServer: true });
ws.handleUpgrade(req, socket, head, (websocket) => {
openShellSession({ websocket, cluster, tabId, nodeName })
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
});
}
};
},
});
export default shellApiRequestInjectable;

View File

@ -1,36 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import logger from "../../../logger";
import { Server as WebSocketServer } from "ws";
import type { ProxyApiRequestArgs } from "../types";
import type { ClusterManager } from "../../../cluster/manager";
import URLParse from "url-parse";
import type { ClusterId } from "../../../../common/cluster-types";
import type { OpenShellSession } from "../../../shell-session/create-shell-session.injectable";
interface Dependencies {
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
openShellSession: OpenShellSession;
clusterManager: ClusterManager;
}
export const shellApiRequest = ({ openShellSession, authenticateRequest, clusterManager }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
const cluster = clusterManager.getClusterForRequest(req);
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
if (!tabId || !cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
socket.write("Invalid shell request");
return void socket.end();
}
const ws = new WebSocketServer({ noServer: true });
ws.handleUpgrade(req, socket, head, (websocket) => {
openShellSession({ websocket, cluster, tabId, nodeName })
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
});
};

View File

@ -4,13 +4,13 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import createLensWindowInjectable from "./create-lens-window.injectable";
import lensProxyPortInjectable from "../../../lens-proxy/lens-proxy-port.injectable";
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
import waitUntilBundledExtensionsAreLoadedInjectable from "./wait-until-bundled-extensions-are-loaded.injectable";
import { applicationWindowInjectionToken } from "./application-window-injection-token";
import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable";
import { runInAction } from "mobx";
import appNameInjectable from "../../../../common/vars/app-name.injectable";
import windowFilePathInjectable from "../../../../common/vars/window-file-path.injectable";
const createApplicationWindowInjectable = getInjectable({
id: "create-application-window",
@ -24,8 +24,8 @@ const createApplicationWindowInjectable = getInjectable({
const isMac = di.inject(isMacInjectable);
const applicationName = di.inject(appNameInjectable);
const waitUntilBundledExtensionsAreLoaded = di.inject(waitUntilBundledExtensionsAreLoadedInjectable);
const lensProxyPort = di.inject(lensProxyPortInjectable);
const emitAppEvent = di.inject(emitAppEventInjectable);
const windowFilePath = di.inject(windowFilePathInjectable);
return createLensWindow({
id,
@ -33,7 +33,7 @@ const createApplicationWindowInjectable = getInjectable({
defaultHeight: 900,
defaultWidth: 1440,
getContentSource: () => ({
url: `https://localhost:${lensProxyPort.get()}`,
file: windowFilePath,
}),
resizable: true,
windowFrameUtilitiesAreShown: isMac,

View File

@ -1,36 +0,0 @@
/**
* 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 { ClusterIdDetector } from "../../cluster-detectors/cluster-id-detector";
import { LastSeenDetector } from "../../cluster-detectors/last-seen-detector";
import { VersionDetector } from "../../cluster-detectors/version-detector";
import { DistributionDetector } from "../../cluster-detectors/distribution-detector";
import { NodesCountDetector } from "../../cluster-detectors/nodes-count-detector";
import detectorRegistryInjectable from "../../cluster-detectors/detector-registry.injectable";
import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token";
const setupDetectorRegistryInjectable = getInjectable({
id: "setup-detector-registry",
instantiate: (di) => {
const detectorRegistry = di.inject(detectorRegistryInjectable);
return {
id: "setup-detector-registry",
run: () => {
detectorRegistry
.add(ClusterIdDetector)
.add(LastSeenDetector)
.add(VersionDetector)
.add(DistributionDetector)
.add(NodesCountDetector);
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default setupDetectorRegistryInjectable;

View File

@ -4,6 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import staticFilesDirectoryInjectable from "../../../common/vars/static-files-directory.injectable";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import { camelCase, flow, upperFirst } from "lodash/fp";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
@ -15,11 +16,12 @@ const getTrayIconPathInjectable = getInjectable({
instantiate: (di) => {
const joinPaths = di.inject(joinPathsInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const isDevelopment = di.inject(isDevelopmentInjectable);
const isMac = di.inject(isMacInjectable);
const baseIconDirectory = joinPaths(
staticFilesDirectory,
"build/tray",
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
);
const fileSuffix = isMac ? "Template.png" : ".png";

View File

@ -28,6 +28,7 @@ import assert from "assert";
import startFrameInjectable from "./start-frame/start-frame.injectable";
export async function bootstrap(di: DiContainer) {
console.log(window.frameElement);
const startFrame = di.inject(startFrameInjectable);
await startFrame();

View File

@ -3,11 +3,21 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getClusterIdFromHost } from "../../common/utils";
import { TypedRegEx } from "typed-regex";
const clusterIdMatcher = TypedRegEx("^cluster-frame-(?<clusterId>.+)$");
const hostedClusterIdInjectable = getInjectable({
id: "hosted-cluster-id",
instantiate: () => getClusterIdFromHost(location.host),
instantiate: () => {
const { frameElement } = window;
if (!frameElement) {
return undefined;
}
return clusterIdMatcher.match(frameElement.id).groups?.clusterId;
},
causesSideEffects: true,
});

View File

@ -29,9 +29,9 @@ const createClusterInjectable = getInjectable({
createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
requestNamespaceListPermissionsFor: () => { throw new Error("Tried to access back-end feature in front-end."); },
createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); },
requestApiResources: ()=> { throw new Error("Tried to access back-end feature in front-end."); },
detectorRegistry: undefined as never,
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
requestApiResources: () => { throw new Error("Tried to access back-end feature in front-end."); },
clusterVersionDetector: undefined as never,
detectClusterMetadata: () => { throw new Error("Tried to access back-end feature in front-end."); },
};
return (model, configData) => new Cluster(dependencies, model, configData);

View File

@ -92,7 +92,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
? [key, processValue(value)] as const
: undefined
))
.collect(fromEntries);
.collect(arg => fromEntries(arg));
};
createSecret = async () => {

View File

@ -5,6 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import windowFilePathInjectable from "../../../common/vars/window-file-path.injectable";
import { ClusterFrameHandler } from "./cluster-frame-handler";
import emitClusterVisibilityInjectable from "./emit-cluster-visibility.injectable";
@ -14,6 +15,7 @@ const clusterFrameHandlerInjectable = getInjectable({
emitClusterVisibility: di.inject(emitClusterVisibilityInjectable),
getClusterById: di.inject(getClusterByIdInjectable),
logger: di.inject(loggerInjectable),
windowFilePath: di.inject(windowFilePathInjectable),
}),
});

View File

@ -6,7 +6,7 @@
import { action, makeObservable, observable, when } from "mobx";
import type { ClusterId } from "../../../common/cluster-types";
import type { Disposer } from "../../utils";
import { getClusterFrameUrl, onceDefined } from "../../utils";
import { onceDefined } from "../../utils";
import assert from "assert";
import type { Logger } from "../../../common/logger";
import type { GetClusterById } from "../../../common/cluster-store/get-by-id.injectable";
@ -19,6 +19,7 @@ export interface LensView {
interface Dependencies {
readonly logger: Logger;
readonly windowFilePath: string;
getClusterById: GetClusterById;
emitClusterVisibility: EmitClusterVisibility;
}
@ -57,7 +58,7 @@ export class ClusterFrameHandler {
iframe.id = `cluster-frame-${cluster.id}`;
iframe.name = cluster.contextName;
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
iframe.setAttribute("src", this.dependencies.windowFilePath);
iframe.addEventListener("load", action(() => {
this.dependencies.logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`);
const view = this.views.get(clusterId);

View File

@ -132,7 +132,10 @@ class NonInjectedKubeObjectListLayout<
onDetails,
...layoutProps
} = this.props;
const placeholderString = ResourceNames[ResourceKindMap[store.api.kind]] || store.api.kind;
const kind = ResourceKindMap[store.api.kind];
const placeholderString = kind
? ResourceNames[kind] || store.api.kind
: store.api.kind;
return (
<ItemListLayout<K, false>

View File

@ -12,7 +12,8 @@ import isDevelopmentInjectable from "../../common/vars/is-development.injectable
import showErrorNotificationInjectable from "../components/notifications/show-error-notification.injectable";
import windowLocationInjectable from "../../common/k8s-api/window-location.injectable";
import { lensAuthenticationHeaderValueInjectionToken } from "../../common/auth/header-value";
import { lensAuthenticationHeader } from "../../common/vars/auth-header";
import { lensAuthenticationHeader, lensClusterIdHeader } from "../../common/vars/auth-header";
import hostedClusterIdInjectable from "../cluster-frame-context/hosted-cluster-id.injectable";
const apiKubeInjectable = getInjectable({
id: "api-kube",
@ -23,6 +24,9 @@ const apiKubeInjectable = getInjectable({
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const { port, host } = di.inject(windowLocationInjectable);
const lensAuthenticationHeaderValue = di.inject(lensAuthenticationHeaderValueInjectionToken);
const hostedClusterId = di.inject(hostedClusterIdInjectable);
assert(hostedClusterId);
const apiKube = createKubeJsonApi({
serverAddress: `https://127.0.0.1:${port}`,
@ -32,6 +36,7 @@ const apiKubeInjectable = getInjectable({
headers: {
"Host": host,
[lensAuthenticationHeader]: lensAuthenticationHeaderValue,
[lensClusterIdHeader]: hostedClusterId,
},
});

View File

@ -23,16 +23,15 @@ const extensionPageParametersInjectable = getInjectable({
const createPageParam = di.inject(createPageParamInjectable);
return pipeline(
registration.params ?? {},
Object.entries,
object.entries(registration.params ?? {}),
map(([key, value]): [string, PageParamInit<unknown>] => [
key,
`${key}`,
typeof value === "string"
? convertStringToPageParamInit(key, value)
: convertPartialPageParamInitToFull(key, value),
? convertStringToPageParamInit(`${key}`, value)
: convertPartialPageParamInitToFull(`${key}`, value),
]),
map(([key, value]) => [key, createPageParam(value)] as const),
object.fromEntries,
arg => object.fromEntries(arg),
);
},
@ -46,7 +45,7 @@ const extensionPageParametersInjectable = getInjectable({
const convertPartialPageParamInitToFull = <V>(
key: string,
value: PageParamInit<V>,
value: Omit<PageParamInit<V>, "name">,
): PageParamInit<V> => ({
name: key,
defaultValue: value.defaultValue,

View File

@ -7,7 +7,7 @@ import type { PageParam } from "../navigation";
import type { ExtensionPageParametersInstantiationParam } from "./extension-page-parameters.injectable";
import extensionPageParametersInjectable from "./extension-page-parameters.injectable";
export type GetExtensionPageParameters = (param: ExtensionPageParametersInstantiationParam) => Record<string, PageParam<unknown>>;
export type GetExtensionPageParameters = (param: ExtensionPageParametersInstantiationParam) => Partial<Record<string, PageParam<unknown>>>;
const getExtensionPageParametersInjectable = getInjectable({
id: "get-extension-page-parameters",

View File

@ -18,7 +18,7 @@ export const appName = isDevelopment
? `${packageInfo.productName}Dev`
: packageInfo.productName;
export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const publicPath = "/build/";
export const publicPath = `${path.resolve(`${__dirname}/../static/build`)}/`;
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
export const webpackDevServerPort = Number(process.env.WEBPACK_DEV_SERVER_PORT) || 9191;

View File

@ -2512,14 +2512,7 @@
resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.29.tgz#68ccecec3d4ffdafb9c577fe764f912afc050fe6"
integrity sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==
"@types/request-promise-native@^1.0.18":
version "1.0.18"
resolved "https://registry.yarnpkg.com/@types/request-promise-native/-/request-promise-native-1.0.18.tgz#437ee2d0b772e01c9691a983b558084b4b3efc2c"
integrity sha512-tPnODeISFc/c1LjWyLuZUY+Z0uLB3+IMfNoQyDEi395+j6kTFTTRAqjENjoPJUid4vHRGEozoTrcTrfZM+AcbA==
dependencies:
"@types/request" "*"
"@types/request@*", "@types/request@^2.47.1", "@types/request@^2.48.7":
"@types/request@^2.47.1":
version "2.48.8"
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c"
integrity sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==
@ -11021,23 +11014,7 @@ repeat-string@^1.5.2, repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
request-promise-core@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
dependencies:
lodash "^4.17.19"
request-promise-native@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
dependencies:
request-promise-core "1.1.4"
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
request@^2.88.0, request@^2.88.2:
request@^2.88.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@ -11843,11 +11820,6 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
stealthy-require@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stream-buffers@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521"
@ -12356,14 +12328,6 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
@ -12373,6 +12337,14 @@ tough-cookie@^4.0.0:
punycode "^2.1.1"
universalify "^0.1.2"
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tr46@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
@ -12963,7 +12935,7 @@ webpack-dev-middleware@^5.3.1:
range-parser "^1.2.1"
schema-utils "^4.0.0"
webpack-dev-server@*, webpack-dev-server@^4.11.1:
webpack-dev-server@*:
version "4.11.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz#ae07f0d71ca0438cf88446f09029b92ce81380b5"
integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==