mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Cleanup 'Cluster' to remove environment specific details (#6951)
- requestNamespaceListPermissions is infallable so no need to have the extra try/catch - Refactor isMetricHidden method away from Cluster - Refactor shouldShowResource out of Cluster - Refactor isInLocalKubeconfig out of Cluster - Remove depecrated and unused workspace from Cluster - Refactor out kubectl as a dependency of Cluster - Remove from cluster getter used only once - Split out ClusterConnection from Cluster - Also split out KubeAuthProxyServer from ContextHandler - Rename ContextHandler to PrometheusHandler - Cleanup onNetworkOffline/Online impls within ClusterManager - Remove annotations from ClusterConnection - Remove mobx annotations from Cluster - Rename loadConfigFromFileInjectable - Remove all uses of dead createClusterInjectionToken - Fix type errors related to broadcastConnectionUpdate Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
e6d6d1d8f7
commit
ad9bafe2a5
133 files changed
+2544
-1961
No files matched your search
@@ -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, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
|
||||
|
||||
const kubeAuthProxyUrlInjectable = getInjectable({
|
||||
id: "kube-auth-proxy-url",
|
||||
instantiate: (di, cluster) => {
|
||||
const lensProxyPort = di.inject(lensProxyPortInjectable);
|
||||
|
||||
return `https://127.0.0.1:${lensProxyPort.get()}/${cluster.id}`;
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, cluster: Cluster) => cluster.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default kubeAuthProxyUrlInjectable;
|
||||
@@ -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, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { KubeAuthUpdate } from "../../common/cluster-types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
|
||||
export type BroadcastConnectionUpdate = (update: KubeAuthUpdate) => void;
|
||||
|
||||
const broadcastConnectionUpdateInjectable = getInjectable({
|
||||
id: "broadcast-connection-update",
|
||||
instantiate: (di, cluster): BroadcastConnectionUpdate => {
|
||||
const broadcastMessage = di.inject(broadcastMessageInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return (update) => {
|
||||
logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: cluster.getMeta() });
|
||||
broadcastMessage(`cluster:${cluster.id}:connection-update`, update);
|
||||
};
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, cluster: Cluster) => cluster.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default broadcastConnectionUpdateInjectable;
|
||||
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { type KubeConfig, HttpError } from "@kubernetes/client-node";
|
||||
import { reaction, comparer, runInAction } from "mobx";
|
||||
import { ClusterStatus } from "../../common/cluster-types";
|
||||
import type { CreateAuthorizationReview } from "../../common/cluster/authorization-review.injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { CreateListNamespaces } from "../../common/cluster/list-namespaces.injectable";
|
||||
import type { RequestNamespaceListPermissionsFor, RequestNamespaceListPermissions } from "../../common/cluster/request-namespace-list-permissions.injectable";
|
||||
import type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable";
|
||||
import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { KubeApiResource } from "../../common/rbac";
|
||||
import { formatKubeApiResource } from "../../common/rbac";
|
||||
import { disposer, isDefined, isRequestError } from "../../common/utils";
|
||||
import { withConcurrencyLimit } from "../../common/utils/with-concurrency-limit";
|
||||
import type { ClusterPrometheusHandler } from "./prometheus-handler/prometheus-handler";
|
||||
import type { BroadcastConnectionUpdate } from "./broadcast-connection-update.injectable";
|
||||
import type { KubeAuthProxyServer } from "./kube-auth-proxy-server.injectable";
|
||||
import type { LoadProxyKubeconfig } from "./load-proxy-kubeconfig.injectable";
|
||||
import type { RemoveProxyKubeconfig } from "./remove-proxy-kubeconfig.injectable";
|
||||
import type { RequestApiResources } from "./request-api-resources.injectable";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
|
||||
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
|
||||
import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||
import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable";
|
||||
import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable";
|
||||
import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable";
|
||||
import requestApiResourcesInjectable from "./request-api-resources.injectable";
|
||||
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
|
||||
import type { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable";
|
||||
import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token";
|
||||
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
|
||||
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
|
||||
import { replaceObservableObject } from "../../common/utils/replace-observable-object";
|
||||
|
||||
interface Dependencies {
|
||||
readonly logger: Logger;
|
||||
readonly prometheusHandler: ClusterPrometheusHandler;
|
||||
readonly kubeAuthProxyServer: KubeAuthProxyServer;
|
||||
readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector;
|
||||
createAuthorizationReview: CreateAuthorizationReview;
|
||||
requestApiResources: RequestApiResources;
|
||||
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor;
|
||||
createListNamespaces: CreateListNamespaces;
|
||||
detectClusterMetadata: DetectClusterMetadata;
|
||||
broadcastMessage: BroadcastMessage;
|
||||
broadcastConnectionUpdate: BroadcastConnectionUpdate;
|
||||
loadProxyKubeconfig: LoadProxyKubeconfig;
|
||||
removeProxyKubeconfig: RemoveProxyKubeconfig;
|
||||
}
|
||||
|
||||
export type { ClusterConnection };
|
||||
|
||||
class ClusterConnection {
|
||||
protected readonly eventsDisposer = disposer();
|
||||
|
||||
protected activated = false;
|
||||
|
||||
constructor(
|
||||
private readonly dependencies: Dependencies,
|
||||
private readonly cluster: Cluster,
|
||||
) {}
|
||||
|
||||
private bindEvents() {
|
||||
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.cluster.getMeta());
|
||||
const refreshTimer = setInterval(() => {
|
||||
if (!this.cluster.disconnected.get()) {
|
||||
this.refresh();
|
||||
}
|
||||
}, 30_000); // every 30s
|
||||
const refreshMetadataTimer = setInterval(() => {
|
||||
if (this.cluster.available.get()) {
|
||||
this.refreshAccessibilityAndMetadata();
|
||||
}
|
||||
}, 900000); // every 15 minutes
|
||||
|
||||
this.eventsDisposer.push(
|
||||
reaction(
|
||||
() => this.cluster.prometheusPreferences.get(),
|
||||
preferences => this.dependencies.prometheusHandler.setupPrometheus(preferences),
|
||||
{ equals: comparer.structural },
|
||||
),
|
||||
() => clearInterval(refreshTimer),
|
||||
() => clearInterval(refreshMetadataTimer),
|
||||
reaction(() => this.cluster.preferences.defaultNamespace, () => this.recreateProxyKubeconfig()),
|
||||
);
|
||||
}
|
||||
|
||||
protected async recreateProxyKubeconfig() {
|
||||
this.dependencies.logger.info("[CLUSTER]: Recreating proxy kubeconfig");
|
||||
|
||||
try {
|
||||
await this.dependencies.removeProxyKubeconfig();
|
||||
await this.dependencies.loadProxyKubeconfig();
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param force force activation
|
||||
*/
|
||||
async activate(force = false) {
|
||||
if (this.activated && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dependencies.logger.info(`[CLUSTER]: activate`, this.cluster.getMeta());
|
||||
|
||||
if (!this.eventsDisposer.length) {
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
if (this.cluster.disconnected || !this.cluster.accessible.get()) {
|
||||
try {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "info",
|
||||
message: "Starting connection ...",
|
||||
});
|
||||
await this.reconnect();
|
||||
} catch (error) {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: `Failed to start connection: ${error}`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "info",
|
||||
message: "Refreshing connection status ...",
|
||||
});
|
||||
await this.refreshConnectionStatus();
|
||||
} catch (error) {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: `Failed to connection status: ${error}`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cluster.accessible.get()) {
|
||||
try {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "info",
|
||||
message: "Refreshing cluster accessibility ...",
|
||||
});
|
||||
await this.refreshAccessibility();
|
||||
} catch (error) {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: `Failed to refresh accessibility: ${error}`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "info",
|
||||
message: "Connected, waiting for view to load ...",
|
||||
});
|
||||
}
|
||||
|
||||
this.activated = true;
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
this.dependencies.logger.info(`[CLUSTER]: reconnect`, this.cluster.getMeta());
|
||||
await this.dependencies.kubeAuthProxyServer?.restart();
|
||||
|
||||
runInAction(() => {
|
||||
this.cluster.disconnected.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.cluster.disconnected) {
|
||||
return this.dependencies.logger.debug("[CLUSTER]: already disconnected", { id: this.cluster.id });
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.dependencies.logger.info(`[CLUSTER]: disconnecting`, { id: this.cluster.id });
|
||||
this.eventsDisposer();
|
||||
this.dependencies.kubeAuthProxyServer?.stop();
|
||||
this.cluster.disconnected.set(true);
|
||||
this.cluster.online.set(false);
|
||||
this.cluster.accessible.set(false);
|
||||
this.cluster.ready.set(false);
|
||||
this.activated = false;
|
||||
this.cluster.allowedNamespaces.clear();
|
||||
this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.cluster.id });
|
||||
});
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.cluster.getMeta());
|
||||
await this.refreshConnectionStatus();
|
||||
}
|
||||
|
||||
async refreshAccessibilityAndMetadata() {
|
||||
await this.refreshAccessibility();
|
||||
await this.refreshMetadata();
|
||||
}
|
||||
|
||||
async refreshMetadata() {
|
||||
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.cluster.getMeta());
|
||||
const metadata = await this.dependencies.detectClusterMetadata(this.cluster);
|
||||
|
||||
runInAction(() => {
|
||||
replaceObservableObject(this.cluster.metadata, metadata);
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshAccessibility(): Promise<void> {
|
||||
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta());
|
||||
const proxyConfig = await this.dependencies.loadProxyKubeconfig();
|
||||
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
|
||||
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig);
|
||||
|
||||
const isAdmin = await canI({
|
||||
namespace: "kube-system",
|
||||
resource: "*",
|
||||
verb: "create",
|
||||
});
|
||||
const isGlobalWatchEnabled = await canI({
|
||||
verb: "watch",
|
||||
resource: "*",
|
||||
});
|
||||
const allowedNamespaces = await this.requestAllowedNamespaces(proxyConfig);
|
||||
const knownResources = await (async () => {
|
||||
const result = await this.dependencies.requestApiResources(this.cluster);
|
||||
|
||||
if (result.callWasSuccessful) {
|
||||
return result.response;
|
||||
}
|
||||
|
||||
if (this.cluster.knownResources.length > 0) {
|
||||
this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources, sticking with previous list`);
|
||||
|
||||
return this.cluster.knownResources;
|
||||
}
|
||||
|
||||
this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources for the first time, blocking connection to cluster...`);
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: "Failed to list kube API resources, please reconnect...",
|
||||
});
|
||||
|
||||
return [];
|
||||
})();
|
||||
const resourcesToShow = await this.getResourcesToShow(allowedNamespaces, knownResources, requestNamespaceListPermissions);
|
||||
|
||||
runInAction(() => {
|
||||
this.cluster.isAdmin.set(isAdmin);
|
||||
this.cluster.isGlobalWatchEnabled.set(isGlobalWatchEnabled);
|
||||
this.cluster.allowedNamespaces.replace(allowedNamespaces);
|
||||
this.cluster.knownResources.replace(knownResources);
|
||||
this.cluster.resourcesToShow.replace(resourcesToShow);
|
||||
this.cluster.ready.set(this.cluster.knownResources.length > 0);
|
||||
});
|
||||
|
||||
this.dependencies.logger.debug(`[CLUSTER]: refreshed accessibility data`, this.cluster.getState());
|
||||
}
|
||||
|
||||
async refreshConnectionStatus() {
|
||||
const connectionStatus = await this.getConnectionStatus();
|
||||
|
||||
runInAction(() => {
|
||||
this.cluster.online.set(connectionStatus > ClusterStatus.Offline);
|
||||
this.cluster.accessible.set(connectionStatus == ClusterStatus.AccessGranted);
|
||||
});
|
||||
}
|
||||
|
||||
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
||||
try {
|
||||
const versionData = await this.dependencies.clusterVersionDetector.detect(this.cluster);
|
||||
|
||||
runInAction(() => {
|
||||
this.cluster.metadata.version = versionData.value;
|
||||
});
|
||||
|
||||
return ClusterStatus.AccessGranted;
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.cluster.contextName.get()}": ${error}`);
|
||||
|
||||
if (isRequestError(error)) {
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: "Invalid credentials",
|
||||
});
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
const message = String(error.error || error.message) || String(error);
|
||||
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message,
|
||||
});
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: "Connection timed out",
|
||||
});
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: "Failed to fetch credentials",
|
||||
});
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
const message = String(error.error || error.message) || String(error);
|
||||
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message,
|
||||
});
|
||||
} else if (error instanceof Error || typeof error === "string") {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: `${error}`,
|
||||
});
|
||||
} else {
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: "Unknown error has occurred",
|
||||
});
|
||||
}
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
}
|
||||
|
||||
protected async requestAllowedNamespaces(proxyConfig: KubeConfig) {
|
||||
if (this.cluster.accessibleNamespaces.length) {
|
||||
return this.cluster.accessibleNamespaces;
|
||||
}
|
||||
|
||||
try {
|
||||
const listNamespaces = this.dependencies.createListNamespaces(proxyConfig);
|
||||
|
||||
return await listNamespaces();
|
||||
} catch (error) {
|
||||
const ctx = proxyConfig.getContextObject(this.cluster.contextName.get());
|
||||
const namespaceList = [ctx?.namespace].filter(isDefined);
|
||||
|
||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||
const { response } = error as HttpError & { response: Response };
|
||||
|
||||
this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.cluster.id, error: response.body });
|
||||
this.dependencies.broadcastMessage(clusterListNamespaceForbiddenChannel, this.cluster.id);
|
||||
}
|
||||
|
||||
return namespaceList;
|
||||
}
|
||||
}
|
||||
|
||||
protected async getResourcesToShow(allowedNamespaces: string[], knownResources: KubeApiResource[], req: RequestNamespaceListPermissions) {
|
||||
if (!allowedNamespaces.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const requestNamespaceListPermissions = withConcurrencyLimit(5)(req);
|
||||
const namespaceListPermissions = allowedNamespaces.map(requestNamespaceListPermissions);
|
||||
const canListResources = await Promise.all(namespaceListPermissions);
|
||||
|
||||
return knownResources
|
||||
.filter((resource) => canListResources.some(fn => fn(resource)))
|
||||
.map(formatKubeApiResource);
|
||||
}
|
||||
}
|
||||
|
||||
const clusterConnectionInjectable = getInjectable({
|
||||
id: "cluster-connection",
|
||||
instantiate: (di, cluster) => new ClusterConnection(
|
||||
{
|
||||
clusterVersionDetector: di.inject(clusterVersionDetectorInjectable),
|
||||
kubeAuthProxyServer: di.inject(kubeAuthProxyServerInjectable, cluster),
|
||||
logger: di.inject(loggerInjectable),
|
||||
prometheusHandler: di.inject(prometheusHandlerInjectable, cluster),
|
||||
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
|
||||
broadcastMessage: di.inject(broadcastMessageInjectable),
|
||||
createAuthorizationReview: di.inject(createAuthorizationReviewInjectable),
|
||||
createListNamespaces: di.inject(createListNamespacesInjectable),
|
||||
detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
|
||||
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
|
||||
removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster),
|
||||
requestApiResources: di.inject(requestApiResourcesInjectable),
|
||||
requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable),
|
||||
},
|
||||
cluster,
|
||||
),
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, cluster: Cluster) => cluster.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default clusterConnectionInjectable;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ServerOptions } from "http-proxy";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable";
|
||||
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
|
||||
|
||||
export interface KubeAuthProxyServer {
|
||||
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;
|
||||
ensureAuthProxyUrl(): Promise<string>;
|
||||
restart(): Promise<void>;
|
||||
ensureRunning(): Promise<void>;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
const fourHoursInMs = 4 * 60 * 60 * 1000;
|
||||
const thirtySecondsInMs = 30 * 1000;
|
||||
|
||||
const kubeAuthProxyServerInjectable = getInjectable({
|
||||
id: "kube-auth-proxy-server",
|
||||
instantiate: (di, cluster): KubeAuthProxyServer => {
|
||||
const clusterUrl = new URL(cluster.apiUrl.get());
|
||||
|
||||
const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
|
||||
const certificate = di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname);
|
||||
|
||||
let kubeAuthProxy: KubeAuthProxy | undefined = undefined;
|
||||
let apiTarget: ServerOptions | undefined = undefined;
|
||||
|
||||
const ensureServerHelper = async (): Promise<KubeAuthProxy> => {
|
||||
if (!kubeAuthProxy) {
|
||||
const proxyEnv = Object.assign({}, process.env);
|
||||
|
||||
if (cluster.preferences.httpsProxy) {
|
||||
proxyEnv.HTTPS_PROXY = cluster.preferences.httpsProxy;
|
||||
}
|
||||
|
||||
kubeAuthProxy = createKubeAuthProxy(cluster, proxyEnv);
|
||||
}
|
||||
|
||||
await kubeAuthProxy.run();
|
||||
|
||||
return kubeAuthProxy;
|
||||
};
|
||||
|
||||
const newApiTarget = async (timeout: number): Promise<ServerOptions> => {
|
||||
const kubeAuthProxy = await ensureServerHelper();
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (clusterUrl.hostname) {
|
||||
headers.Host = clusterUrl.hostname;
|
||||
|
||||
// fix current IPv6 inconsistency in url.Parse() and httpProxy.
|
||||
// with url.Parse the IPv6 Hostname has no Square brackets but httpProxy needs the Square brackets to work.
|
||||
if (headers.Host.includes(":")) {
|
||||
headers.Host = `[${headers.Host}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
target: {
|
||||
protocol: "https:",
|
||||
host: "127.0.0.1",
|
||||
port: kubeAuthProxy.port,
|
||||
path: kubeAuthProxy.apiPrefix,
|
||||
ca: certificate.cert,
|
||||
},
|
||||
changeOrigin: true,
|
||||
timeout,
|
||||
secure: true,
|
||||
headers,
|
||||
};
|
||||
};
|
||||
|
||||
const stopServer = () => {
|
||||
kubeAuthProxy?.exit();
|
||||
kubeAuthProxy = undefined;
|
||||
apiTarget = undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
getApiTarget: async (isLongRunningRequest = false) => {
|
||||
if (isLongRunningRequest) {
|
||||
return newApiTarget(fourHoursInMs);
|
||||
}
|
||||
|
||||
return apiTarget ??= await newApiTarget(thirtySecondsInMs);
|
||||
},
|
||||
ensureAuthProxyUrl: async () => {
|
||||
const kubeAuthProxy = await ensureServerHelper();
|
||||
|
||||
return `https://127.0.0.1:${kubeAuthProxy.port}${kubeAuthProxy.apiPrefix}`;
|
||||
},
|
||||
ensureRunning: async () => {
|
||||
await ensureServerHelper();
|
||||
},
|
||||
restart: async () => {
|
||||
stopServer();
|
||||
await ensureServerHelper();
|
||||
},
|
||||
stop: stopServer,
|
||||
};
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, cluster: Cluster) => cluster.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default kubeAuthProxyServerInjectable;
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import loadConfigFromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable";
|
||||
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
|
||||
export type LoadProxyKubeconfig = () => Promise<KubeConfig>;
|
||||
|
||||
const loadProxyKubeconfigInjectable = getInjectable({
|
||||
id: "load-proxy-kubeconfig",
|
||||
instantiate: (di, cluster) => {
|
||||
const loadConfigFromFile = di.inject(loadConfigFromFileInjectable);
|
||||
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
|
||||
|
||||
return async () => {
|
||||
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
|
||||
const { config } = await loadConfigFromFile(proxyKubeconfigPath);
|
||||
|
||||
return config;
|
||||
};
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, cluster: Cluster) => cluster.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default loadProxyKubeconfigInjectable;
|
||||
@@ -7,6 +7,7 @@ import clusterStoreInjectable from "../../common/cluster-store/cluster-store.inj
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
|
||||
import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable";
|
||||
import clusterConnectionInjectable from "./cluster-connection.injectable";
|
||||
import { ClusterManager } from "./manager";
|
||||
import updateEntityMetadataInjectable from "./update-entity-metadata.injectable";
|
||||
import updateEntitySpecInjectable from "./update-entity-spec.injectable";
|
||||
@@ -23,6 +24,7 @@ const clusterManagerInjectable = getInjectable({
|
||||
logger: di.inject(loggerInjectable),
|
||||
updateEntityMetadata: di.inject(updateEntityMetadataInjectable),
|
||||
updateEntitySpec: di.inject(updateEntitySpecInjectable),
|
||||
getClusterConnection: (cluster) => di.inject(clusterConnectionInjectable, cluster),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { CatalogEntityRegistry } from "../catalog";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable";
|
||||
import type { UpdateEntitySpec } from "./update-entity-spec.injectable";
|
||||
import type { ClusterConnection } from "./cluster-connection.injectable";
|
||||
|
||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
|
||||
@@ -28,8 +29,9 @@ interface Dependencies {
|
||||
readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
|
||||
readonly visibleCluster: IObservableValue<ClusterId | null>;
|
||||
readonly logger: Logger;
|
||||
readonly updateEntityMetadata: UpdateEntityMetadata;
|
||||
readonly updateEntitySpec: UpdateEntitySpec;
|
||||
updateEntityMetadata: UpdateEntityMetadata;
|
||||
updateEntitySpec: UpdateEntitySpec;
|
||||
getClusterConnection: (cluster: Cluster) => ClusterConnection;
|
||||
}
|
||||
|
||||
export class ClusterManager {
|
||||
@@ -119,13 +121,13 @@ export class ClusterManager {
|
||||
return LensKubernetesClusterStatus.DISCONNECTED;
|
||||
}
|
||||
|
||||
if (cluster.accessible) {
|
||||
if (cluster.accessible.get()) {
|
||||
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTED, reason="cluster is accessible"`);
|
||||
|
||||
return LensKubernetesClusterStatus.CONNECTED;
|
||||
}
|
||||
|
||||
if (!cluster.disconnected) {
|
||||
if (!cluster.disconnected.get()) {
|
||||
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTING, reason="cluster is not disconnected"`);
|
||||
|
||||
return LensKubernetesClusterStatus.CONNECTING;
|
||||
@@ -174,8 +176,8 @@ export class ClusterManager {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||
cluster.contextName = entity.spec.kubeconfigContext;
|
||||
cluster.kubeConfigPath.set(entity.spec.kubeconfigPath);
|
||||
cluster.contextName.set(entity.spec.kubeconfigContext);
|
||||
|
||||
if (entity.spec.accessibleNamespaces) {
|
||||
cluster.accessibleNamespaces.replace(entity.spec.accessibleNamespaces);
|
||||
@@ -202,30 +204,43 @@ export class ClusterManager {
|
||||
}
|
||||
}
|
||||
|
||||
protected onNetworkOffline = () => {
|
||||
protected onNetworkOffline = async () => {
|
||||
this.dependencies.logger.info(`${logPrefix} network is offline`);
|
||||
this.dependencies.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.online = false;
|
||||
cluster.accessible = false;
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(
|
||||
this.dependencies.store.clustersList
|
||||
.filter(cluster => !cluster.disconnected.get())
|
||||
.map(async (cluster) => {
|
||||
cluster.online.set(false);
|
||||
cluster.accessible.set(false);
|
||||
|
||||
await this.dependencies
|
||||
.getClusterConnection(cluster)
|
||||
.refreshConnectionStatus();
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
protected onNetworkOnline = () => {
|
||||
protected onNetworkOnline = async () => {
|
||||
this.dependencies.logger.info(`${logPrefix} network is online`);
|
||||
this.dependencies.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(
|
||||
this.dependencies.store.clustersList
|
||||
.filter(cluster => !cluster.disconnected.get())
|
||||
.map((cluster) => (
|
||||
this.dependencies
|
||||
.getClusterConnection(cluster)
|
||||
.refreshConnectionStatus()
|
||||
)),
|
||||
);
|
||||
};
|
||||
|
||||
stop() {
|
||||
this.dependencies.store.clusters.forEach((cluster: Cluster) => {
|
||||
cluster.disconnect();
|
||||
});
|
||||
for (const cluster of this.dependencies.store.clustersList) {
|
||||
this.dependencies
|
||||
.getClusterConnection(cluster)
|
||||
.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,26 +248,26 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
||||
return new KubernetesCluster({
|
||||
metadata: {
|
||||
uid: cluster.id,
|
||||
name: cluster.name,
|
||||
name: cluster.name.get(),
|
||||
source: "local",
|
||||
labels: {
|
||||
...cluster.labels,
|
||||
},
|
||||
distro: cluster.distribution,
|
||||
kubeVersion: cluster.version,
|
||||
distro: cluster.distribution.get(),
|
||||
kubeVersion: cluster.version.get(),
|
||||
},
|
||||
spec: {
|
||||
kubeconfigPath: cluster.kubeConfigPath,
|
||||
kubeconfigContext: cluster.contextName,
|
||||
kubeconfigPath: cluster.kubeConfigPath.get(),
|
||||
kubeconfigContext: cluster.contextName.get(),
|
||||
icon: {},
|
||||
},
|
||||
status: {
|
||||
phase: cluster.disconnected
|
||||
phase: cluster.disconnected.get()
|
||||
? LensKubernetesClusterStatus.DISCONNECTED
|
||||
: LensKubernetesClusterStatus.CONNECTED,
|
||||
reason: "",
|
||||
message: "",
|
||||
active: !cluster.disconnected,
|
||||
active: !cluster.disconnected.get(),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { createClusterPrometheusHandler } from "./prometheus-handler";
|
||||
import getPrometheusProviderByKindInjectable from "../../prometheus/get-by-kind.injectable";
|
||||
import prometheusProvidersInjectable from "../../prometheus/providers.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import loadProxyKubeconfigInjectable from "../load-proxy-kubeconfig.injectable";
|
||||
|
||||
const prometheusHandlerInjectable = getInjectable({
|
||||
id: "prometheus-handler",
|
||||
|
||||
instantiate: (di, cluster) => createClusterPrometheusHandler(
|
||||
{
|
||||
getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable),
|
||||
prometheusProviders: di.inject(prometheusProvidersInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
|
||||
},
|
||||
cluster,
|
||||
),
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, cluster: Cluster) => cluster.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default prometheusHandlerInjectable;
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { PrometheusProvider, PrometheusService } from "../../prometheus/provider";
|
||||
import type { ClusterPrometheusPreferences } from "../../../common/cluster-types";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import type { GetPrometheusProviderByKind } from "../../prometheus/get-by-kind.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { Logger } from "../../../common/logger";
|
||||
import type { LoadProxyKubeconfig } from "../load-proxy-kubeconfig.injectable";
|
||||
|
||||
export interface PrometheusDetails {
|
||||
prometheusPath: string;
|
||||
provider: PrometheusProvider;
|
||||
}
|
||||
|
||||
interface PrometheusServicePreferences {
|
||||
namespace: string;
|
||||
service: string;
|
||||
port: number;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
readonly prometheusProviders: IComputedValue<PrometheusProvider[]>;
|
||||
readonly logger: Logger;
|
||||
getPrometheusProviderByKind: GetPrometheusProviderByKind;
|
||||
loadProxyKubeconfig: LoadProxyKubeconfig;
|
||||
}
|
||||
|
||||
export interface ClusterPrometheusHandler {
|
||||
setupPrometheus(preferences?: ClusterPrometheusPreferences): void;
|
||||
getPrometheusDetails(): Promise<PrometheusDetails>;
|
||||
}
|
||||
|
||||
const ensurePrometheusPath = ({ service, namespace, port }: PrometheusService) => `${namespace}/services/${service}:${port}`;
|
||||
|
||||
export const createClusterPrometheusHandler = (...args: [Dependencies, Cluster]): ClusterPrometheusHandler => {
|
||||
const [deps, cluster] = args;
|
||||
const {
|
||||
getPrometheusProviderByKind,
|
||||
loadProxyKubeconfig,
|
||||
logger,
|
||||
prometheusProviders,
|
||||
} = deps;
|
||||
|
||||
let prometheusProvider: string | undefined = undefined;
|
||||
let prometheus: PrometheusServicePreferences | undefined = undefined;
|
||||
|
||||
const setupPrometheus: ClusterPrometheusHandler["setupPrometheus"] = (preferences = {}) => {
|
||||
prometheusProvider = preferences.prometheusProvider?.type;
|
||||
prometheus = preferences.prometheus;
|
||||
};
|
||||
|
||||
const ensurePrometheusProvider = (service: PrometheusService) => {
|
||||
if (!prometheusProvider) {
|
||||
logger.info(`[CONTEXT-HANDLER]: using ${service.kind} as prometheus provider for clusterId=${cluster.id}`);
|
||||
prometheusProvider = service.kind;
|
||||
}
|
||||
|
||||
return getPrometheusProviderByKind(prometheusProvider);
|
||||
};
|
||||
|
||||
const listPotentialProviders = () => {
|
||||
if (prometheusProvider) {
|
||||
const provider = getPrometheusProviderByKind(prometheusProvider);
|
||||
|
||||
if (provider) {
|
||||
return [provider];
|
||||
}
|
||||
}
|
||||
|
||||
return prometheusProviders.get();
|
||||
};
|
||||
|
||||
const getPrometheusService = async (): Promise<PrometheusService> => {
|
||||
setupPrometheus(cluster.preferences);
|
||||
|
||||
if (prometheus && prometheusProvider) {
|
||||
return {
|
||||
kind: prometheusProvider,
|
||||
namespace: prometheus.namespace,
|
||||
service: prometheus.service,
|
||||
port: prometheus.port,
|
||||
};
|
||||
}
|
||||
|
||||
const providers = listPotentialProviders();
|
||||
const proxyConfig = await loadProxyKubeconfig();
|
||||
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
|
||||
const potentialServices = await Promise.allSettled(
|
||||
providers.map(provider => provider.getPrometheusService(apiClient)),
|
||||
);
|
||||
const errors = [];
|
||||
|
||||
for (const res of potentialServices) {
|
||||
switch (res.status) {
|
||||
case "rejected":
|
||||
errors.push(res.reason);
|
||||
break;
|
||||
|
||||
case "fulfilled":
|
||||
if (res.value) {
|
||||
return res.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No Prometheus service found", { cause: errors });
|
||||
};
|
||||
|
||||
const getPrometheusDetails: ClusterPrometheusHandler["getPrometheusDetails"] = async () => {
|
||||
const service = await getPrometheusService();
|
||||
const prometheusPath = ensurePrometheusPath(service);
|
||||
const provider = ensurePrometheusProvider(service);
|
||||
|
||||
return { prometheusPath, provider };
|
||||
};
|
||||
|
||||
setupPrometheus(cluster.preferences);
|
||||
|
||||
return {
|
||||
setupPrometheus,
|
||||
getPrometheusDetails,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
|
||||
export type RemoveProxyKubeconfig = () => Promise<void>;
|
||||
|
||||
const removeProxyKubeconfigInjectable = getInjectable({
|
||||
id: "remove-proxy-kubeconfig",
|
||||
instantiate: (di, cluster): RemoveProxyKubeconfig => {
|
||||
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
|
||||
|
||||
return () => proxyKubeconfigManager.clear();
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, cluster: Cluster) => cluster.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default removeProxyKubeconfigInjectable;
|
||||
@@ -12,6 +12,7 @@ import { withConcurrencyLimit } from "../../common/utils/with-concurrency-limit"
|
||||
import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable";
|
||||
import type { AsyncResult } from "../../common/utils/async-result";
|
||||
import { backoffCaller } from "../../common/utils/backoff-caller";
|
||||
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
|
||||
|
||||
export type RequestApiResources = (cluster: Cluster) => Promise<AsyncResult<KubeApiResource[], Error>>;
|
||||
|
||||
@@ -29,6 +30,7 @@ const requestApiResourcesInjectable = getInjectable({
|
||||
|
||||
return async (...args) => {
|
||||
const [cluster] = args;
|
||||
const broadcastConnectionUpdate = di.inject(broadcastConnectionUpdateInjectable, cluster);
|
||||
const requestKubeApiResources = withConcurrencyLimit(5)(requestKubeApiResourcesFor(cluster));
|
||||
|
||||
const groupLists: KubeResourceListGroup[] = [];
|
||||
@@ -36,7 +38,10 @@ const requestApiResourcesInjectable = getInjectable({
|
||||
for (const apiVersionRequester of apiVersionRequesters) {
|
||||
const result = await backoffCaller(() => apiVersionRequester(cluster), {
|
||||
onIntermediateError: (error, attempt) => {
|
||||
cluster.broadcastConnectUpdate(`Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, "warning");
|
||||
broadcastConnectionUpdate({
|
||||
message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`,
|
||||
level: "warning",
|
||||
});
|
||||
logger.warn(`[LIST-API-RESOURCES]: failed to list kube api resources: ${error}`, { attempt, clusterId: cluster.id });
|
||||
},
|
||||
});
|
||||
@@ -56,7 +61,10 @@ const requestApiResourcesInjectable = getInjectable({
|
||||
|
||||
for (const result of results) {
|
||||
if (!result.callWasSuccessful) {
|
||||
cluster.broadcastConnectUpdate(`Kube APIs under "${result.listGroup.path}" may not be displayed`, "warning");
|
||||
broadcastConnectionUpdate({
|
||||
message: `Kube APIs under "${result.listGroup.path}" may not be displayed`,
|
||||
level: "warning",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ interface Pre500WorkspaceStoreModel {
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Pre500ClusterModel extends ClusterModel {
|
||||
workspace?: string;
|
||||
}
|
||||
|
||||
const v500Beta10ClusterStoreMigrationInjectable = getInjectable({
|
||||
id: "v5.0.0-beta.10-cluster-store-migration",
|
||||
instantiate: (di) => {
|
||||
@@ -35,7 +39,7 @@ const v500Beta10ClusterStoreMigrationInjectable = getInjectable({
|
||||
workspaces.set(id, name);
|
||||
}
|
||||
|
||||
const clusters = (store.get("clusters") ?? []) as ClusterModel[];
|
||||
const clusters = (store.get("clusters") ?? []) as Pre500ClusterModel[];
|
||||
|
||||
for (const cluster of clusters) {
|
||||
if (cluster.workspace) {
|
||||
|
||||
@@ -8,6 +8,15 @@ import { moveSync, removeSync } from "fs-extra";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { isDefined } from "../../../common/utils";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
|
||||
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
|
||||
|
||||
interface Pre500ClusterModel extends ClusterModel {
|
||||
workspace?: string;
|
||||
workspaces?: string[];
|
||||
}
|
||||
|
||||
function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences {
|
||||
if (left.prometheus && left.prometheusProvider) {
|
||||
@@ -27,24 +36,15 @@ function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: C
|
||||
return {};
|
||||
}
|
||||
|
||||
function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences {
|
||||
return {
|
||||
terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
|
||||
clusterName: left.clusterName || right.clusterName || undefined,
|
||||
iconOrder: left.iconOrder || right.iconOrder || undefined,
|
||||
icon: left.icon || right.icon || undefined,
|
||||
httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
|
||||
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
|
||||
...mergePrometheusPreferences(left, right),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeLabels(left: Record<string, string>, right: Record<string, string>): Record<string, string> {
|
||||
return {
|
||||
...right,
|
||||
...left,
|
||||
};
|
||||
}
|
||||
const mergePreferences = (left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences => ({
|
||||
terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
|
||||
clusterName: left.clusterName || right.clusterName || undefined,
|
||||
iconOrder: left.iconOrder || right.iconOrder || undefined,
|
||||
icon: left.icon || right.icon || undefined,
|
||||
httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
|
||||
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
|
||||
...mergePrometheusPreferences(left, right),
|
||||
});
|
||||
|
||||
function mergeSet(...iterables: Iterable<string | undefined>[]): string[] {
|
||||
const res = new Set<string>();
|
||||
@@ -60,24 +60,17 @@ function mergeSet(...iterables: Iterable<string | undefined>[]): string[] {
|
||||
return [...res];
|
||||
}
|
||||
|
||||
function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">): ClusterModel {
|
||||
return {
|
||||
id: prev.id,
|
||||
kubeConfigPath: prev.kubeConfigPath,
|
||||
contextName: prev.contextName,
|
||||
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
|
||||
metadata: prev.metadata,
|
||||
labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}),
|
||||
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
|
||||
workspace: prev.workspace || right.workspace,
|
||||
workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
|
||||
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
|
||||
const mergeClusterModel = (prev: Pre500ClusterModel, right: Omit<Pre500ClusterModel, "id">): Pre500ClusterModel => ({
|
||||
id: prev.id,
|
||||
kubeConfigPath: prev.kubeConfigPath,
|
||||
contextName: prev.contextName,
|
||||
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
|
||||
metadata: prev.metadata,
|
||||
labels: { ...(right.labels ?? {}), ...(prev.labels ?? {}) },
|
||||
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
|
||||
workspace: prev.workspace || right.workspace,
|
||||
workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []),
|
||||
});
|
||||
|
||||
const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
|
||||
id: "v5.0.0-beta.13-cluster-store-migration",
|
||||
@@ -104,8 +97,8 @@ const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
|
||||
version: "5.0.0-beta.13",
|
||||
run(store) {
|
||||
const folder = joinPaths(userDataPath, "lens-local-storage");
|
||||
const oldClusters = (store.get("clusters") ?? []) as ClusterModel[];
|
||||
const clusters = new Map<string, ClusterModel>();
|
||||
const oldClusters = (store.get("clusters") ?? []) as Pre500ClusterModel[];
|
||||
const clusters = new Map<string, Pre500ClusterModel>();
|
||||
|
||||
for (const { id: oldId, ...cluster } of oldClusters) {
|
||||
const newId = generateNewIdFor(cluster);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { toJS } from "mobx";
|
||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
@@ -17,10 +18,10 @@ const updateEntityMetadataInjectable = getInjectable({
|
||||
return (entity, cluster) => {
|
||||
entity.metadata.labels = {
|
||||
...entity.metadata.labels,
|
||||
...cluster.labels,
|
||||
...toJS(cluster.labels),
|
||||
};
|
||||
entity.metadata.distro = cluster.distribution;
|
||||
entity.metadata.kubeVersion = cluster.version;
|
||||
entity.metadata.distro = cluster.distribution.get();
|
||||
entity.metadata.kubeVersion = cluster.version.get();
|
||||
|
||||
enumKeys(ClusterMetadataKey).forEach((key) => {
|
||||
const metadataKey = ClusterMetadataKey[key];
|
||||
|
||||
@@ -7,8 +7,8 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import { replaceObservableObject } from "../../common/utils/replace-observable-object";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable";
|
||||
import updateEntityMetadataInjectable from "./update-entity-metadata.injectable";
|
||||
@@ -27,11 +27,10 @@ describe("update-entity-metadata", () => {
|
||||
get: () => ({} as AppPaths),
|
||||
set: () => {},
|
||||
}));
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
updateEntityMetadata = di.inject(updateEntityMetadataInjectable);
|
||||
|
||||
cluster = createCluster({
|
||||
cluster = new Cluster({
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
@@ -50,10 +49,6 @@ describe("update-entity-metadata", () => {
|
||||
},
|
||||
};
|
||||
|
||||
cluster.metadata = {
|
||||
...cluster.metadata,
|
||||
};
|
||||
|
||||
entity = new KubernetesCluster({
|
||||
metadata: {
|
||||
uid: "some-uid",
|
||||
@@ -125,9 +120,9 @@ describe("update-entity-metadata", () => {
|
||||
});
|
||||
|
||||
it("given cluster has labels, updates entity metadata with labels", () => {
|
||||
cluster.labels = {
|
||||
replaceObservableObject(cluster.labels, {
|
||||
"some-label": "some-value",
|
||||
};
|
||||
});
|
||||
entity.metadata.labels = {
|
||||
"some-other-label": "some-other-value",
|
||||
};
|
||||
@@ -139,9 +134,9 @@ describe("update-entity-metadata", () => {
|
||||
});
|
||||
|
||||
it("given cluster has labels, overwrites entity metadata with cluster labels", () => {
|
||||
cluster.labels = {
|
||||
replaceObservableObject(cluster.labels, {
|
||||
"some-label": "some-cluster-value",
|
||||
};
|
||||
});
|
||||
entity.metadata.labels = {
|
||||
"some-label": "some-entity-value",
|
||||
};
|
||||
|
||||
@@ -6,8 +6,7 @@ import type { AppPaths } from "../../common/app-paths/app-path-injection-token";
|
||||
import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable";
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { UpdateEntitySpec } from "./update-entity-spec.injectable";
|
||||
import updateEntitySpecInjectable from "./update-entity-spec.injectable";
|
||||
@@ -25,11 +24,10 @@ describe("update-entity-spec", () => {
|
||||
get: () => ({} as AppPaths),
|
||||
set: () => {},
|
||||
}));
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
updateEntitySpec = di.inject(updateEntitySpecInjectable);
|
||||
|
||||
cluster = createCluster({
|
||||
cluster = new Cluster({
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
|
||||
Reference in new issue
Block a user