mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Refactor watches to use native k8s api (#2095)
* fix lint Signed-off-by: Roman <ixrock@gmail.com> * fixes & refactoring Signed-off-by: Roman <ixrock@gmail.com> * fix lint, micro-refactoring Signed-off-by: Roman <ixrock@gmail.com> * more refactoring, clean up, responding to comments Signed-off-by: Roman <ixrock@gmail.com> * fix: remove extra check for cluster.allowedApi from processing buffered watch-api events Signed-off-by: Roman <ixrock@gmail.com> * refactoring, detaching NamespaceStore from KubeObjectStore Signed-off-by: Roman <ixrock@gmail.com> * fix: wait for contextReady in NamespaceStore Signed-off-by: Roman <ixrock@gmail.com> * refactoring & fixes Signed-off-by: Roman <ixrock@gmail.com> * fix lint Signed-off-by: Roman <ixrock@gmail.com> * fixes: reloading context stores on NamespaceSelect-change Signed-off-by: Roman <ixrock@gmail.com> * optimize loading all resources when "all namespaces" selected -> single request per resource (when have rights) Signed-off-by: Roman <ixrock@gmail.com> * use native k8s api watches Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * retry watch when it makes sense Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * workaround for browser connection limits Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use always random subdomain for getResponse Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * resubscribe stores on contextNamespace change Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * modify watch event before calling callback Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Co-authored-by: Roman <ixrock@gmail.com>
This commit is contained in:
1 parent
6ded5e73fa
commit
035dd470ef
34 files changed
+414
-599
No files matched your search
@@ -194,7 +194,8 @@ export class LensProxy {
|
||||
|
||||
if (proxyTarget) {
|
||||
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||
res.setHeader("Access-Control-Allow-Origin", this.origin);
|
||||
// this should be safe because we have already validated cluster uuid
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
return proxy.web(req, res, proxyTarget);
|
||||
}
|
||||
|
||||
+1
-4
@@ -5,7 +5,7 @@ import path from "path";
|
||||
import { readFile } from "fs-extra";
|
||||
import { Cluster } from "./cluster";
|
||||
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
||||
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute, versionRoute } from "./routes";
|
||||
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, versionRoute } from "./routes";
|
||||
import logger from "./logger";
|
||||
|
||||
export interface RouterRequestOpts {
|
||||
@@ -146,9 +146,6 @@ export class Router {
|
||||
this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute));
|
||||
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
|
||||
|
||||
// Watch API
|
||||
this.router.add({ method: "post", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute));
|
||||
|
||||
// Metrics API
|
||||
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute));
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from "./kubeconfig-route";
|
||||
export * from "./metrics-route";
|
||||
export * from "./port-forward-route";
|
||||
export * from "./watch-route";
|
||||
export * from "./helm-route";
|
||||
export * from "./resource-applier-route";
|
||||
export * from "./version-route";
|
||||
@@ -1,162 +0,0 @@
|
||||
import type { KubeJsonApiData, KubeJsonApiError } from "../../renderer/api/kube-json-api";
|
||||
|
||||
import plimit from "p-limit";
|
||||
import { delay } from "../../common/utils";
|
||||
import { LensApiRequest } from "../router";
|
||||
import { LensApi } from "../lens-api";
|
||||
import { KubeConfig, Watch } from "@kubernetes/client-node";
|
||||
import { ServerResponse } from "http";
|
||||
import { Request } from "request";
|
||||
import logger from "../logger";
|
||||
|
||||
export interface IKubeWatchEvent<T = KubeJsonApiData | KubeJsonApiError> {
|
||||
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR" | "STREAM_END";
|
||||
object?: T;
|
||||
}
|
||||
|
||||
export interface IKubeWatchEventStreamEnd extends IKubeWatchEvent {
|
||||
type: "STREAM_END";
|
||||
url: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface IWatchRoutePayload {
|
||||
apis: string[]; // kube-api url list for subscribing to watch events
|
||||
}
|
||||
|
||||
class ApiWatcher {
|
||||
private apiUrl: string;
|
||||
private response: ServerResponse;
|
||||
private watchRequest: Request;
|
||||
private watch: Watch;
|
||||
private processor: NodeJS.Timeout;
|
||||
private eventBuffer: any[] = [];
|
||||
|
||||
constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
|
||||
this.apiUrl = apiUrl;
|
||||
this.watch = new Watch(kubeConfig);
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
if (this.processor) {
|
||||
clearInterval(this.processor);
|
||||
}
|
||||
this.processor = setInterval(() => {
|
||||
if (this.response.finished) return;
|
||||
const events = this.eventBuffer.splice(0);
|
||||
|
||||
events.map(event => this.sendEvent(event));
|
||||
this.response.flushHeaders();
|
||||
}, 50);
|
||||
this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this));
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (!this.watchRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.processor) {
|
||||
clearInterval(this.processor);
|
||||
}
|
||||
logger.debug(`Stopping watcher for api: ${this.apiUrl}`);
|
||||
|
||||
try {
|
||||
this.watchRequest.abort();
|
||||
|
||||
const event: IKubeWatchEventStreamEnd = {
|
||||
type: "STREAM_END",
|
||||
url: this.apiUrl,
|
||||
status: 410,
|
||||
};
|
||||
|
||||
this.sendEvent(event);
|
||||
logger.debug("watch aborted");
|
||||
} catch (error) {
|
||||
logger.error(`Watch abort errored:${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private watchHandler(phase: string, obj: any) {
|
||||
this.eventBuffer.push({
|
||||
type: phase,
|
||||
object: obj
|
||||
});
|
||||
}
|
||||
|
||||
private doneHandler(error: Error) {
|
||||
if (error) logger.warn(`watch ended: ${error.toString()}`);
|
||||
this.watchRequest.abort();
|
||||
}
|
||||
|
||||
private sendEvent(evt: IKubeWatchEvent) {
|
||||
this.response.write(`${JSON.stringify(evt)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
class WatchRoute extends LensApi {
|
||||
private response: ServerResponse;
|
||||
|
||||
private setResponse(response: ServerResponse) {
|
||||
// clean up previous connection and stop all corresponding watch-api requests
|
||||
// otherwise it happens only by request timeout or something else..
|
||||
this.response?.destroy();
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public async routeWatch(request: LensApiRequest<IWatchRoutePayload>) {
|
||||
const { response, cluster, payload: { apis } = {} } = request;
|
||||
|
||||
if (!apis?.length) {
|
||||
this.respondJson(response, {
|
||||
message: "watch apis list is empty"
|
||||
}, 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setResponse(response);
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Connection", "keep-alive");
|
||||
logger.debug(`watch using kubeconfig:${JSON.stringify(cluster.getProxyKubeconfig(), null, 2)}`);
|
||||
|
||||
// limit concurrent k8s requests to avoid possible ECONNRESET-error
|
||||
const requests = plimit(5);
|
||||
const watchers = new Map<string, ApiWatcher>();
|
||||
let isWatchRequestEnded = false;
|
||||
|
||||
apis.forEach(apiUrl => {
|
||||
const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
|
||||
|
||||
watchers.set(apiUrl, watcher);
|
||||
|
||||
requests(async () => {
|
||||
if (isWatchRequestEnded) return;
|
||||
await watcher.start();
|
||||
await delay(100);
|
||||
});
|
||||
});
|
||||
|
||||
function onRequestEnd() {
|
||||
if (isWatchRequestEnded) return;
|
||||
isWatchRequestEnded = true;
|
||||
requests.clearQueue();
|
||||
watchers.forEach(watcher => watcher.stop());
|
||||
watchers.clear();
|
||||
}
|
||||
|
||||
request.raw.req.on("end", () => {
|
||||
logger.info("Watch request end");
|
||||
onRequestEnd();
|
||||
});
|
||||
|
||||
request.raw.req.on("close", () => {
|
||||
logger.info("Watch request close");
|
||||
onRequestEnd();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const watchRoute = new WatchRoute();
|
||||
Reference in new issue
Block a user