mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Refactor dashboard server logic to electron main (#146)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
e54b79b56a
commit
1aaa695cfe
38 files changed
+430
-1323
No files matched your search
@@ -1,16 +1,12 @@
|
||||
import { app } from "electron"
|
||||
import { KubeConfig } from "@kubernetes/client-node"
|
||||
import { readFileSync } from "fs"
|
||||
import * as http from "http"
|
||||
import { ServerOptions } from "http-proxy"
|
||||
import * as url from "url"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import logger from "./logger"
|
||||
import { getFreePort } from "./port"
|
||||
import { LensServer } from "./lens-server"
|
||||
import { KubeAuthProxy } from "./kube-auth-proxy"
|
||||
import { Cluster, ClusterPreferences } from "./cluster"
|
||||
import { userStore } from "../common/user-store"
|
||||
|
||||
export class ContextHandler {
|
||||
public contextName: string
|
||||
@@ -24,14 +20,12 @@ export class ContextHandler {
|
||||
protected apiTarget: ServerOptions
|
||||
protected proxyTarget: ServerOptions
|
||||
protected clusterUrl: url.UrlWithStringQuery
|
||||
protected localServer: LensServer
|
||||
protected proxyServer: KubeAuthProxy
|
||||
|
||||
protected clientCert: string
|
||||
protected clientKey: string
|
||||
protected secureApiConnection = true
|
||||
protected defaultNamespace: string
|
||||
protected port: number
|
||||
protected proxyPort: number
|
||||
protected kubernetesApi: string
|
||||
protected prometheusPath: string
|
||||
@@ -86,6 +80,10 @@ export class ContextHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public getPrometheusPath() {
|
||||
return this.prometheusPath
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const currentCluster = this.kc.getCurrentCluster()
|
||||
if (currentCluster.caFile) {
|
||||
@@ -124,41 +122,6 @@ export class ContextHandler {
|
||||
return this.apiTarget
|
||||
}
|
||||
|
||||
public async getProxyTarget() {
|
||||
if (this.proxyTarget) {
|
||||
return this.proxyTarget;
|
||||
}
|
||||
|
||||
this.proxyTarget = {
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
target: {
|
||||
host: this.clusterUrl.host,
|
||||
hostname: "localhost",
|
||||
path: "/",
|
||||
port: await this.resolvePort(),
|
||||
protocol: "http://",
|
||||
},
|
||||
}
|
||||
|
||||
return this.proxyTarget;
|
||||
}
|
||||
|
||||
protected async resolvePort(): Promise<number> {
|
||||
if (this.port) return this.port
|
||||
|
||||
let serverPort: number = null
|
||||
try {
|
||||
serverPort = await getFreePort(49153, 49900) // the proxy will usually already be on 49152 so skip that
|
||||
} catch(error) {
|
||||
logger.error(error)
|
||||
throw(error)
|
||||
}
|
||||
this.port = serverPort
|
||||
|
||||
return serverPort
|
||||
}
|
||||
|
||||
protected async resolveProxyPort(): Promise<number> {
|
||||
if (this.proxyPort) return this.proxyPort
|
||||
|
||||
@@ -186,35 +149,7 @@ export class ContextHandler {
|
||||
}
|
||||
}
|
||||
|
||||
protected initServer(serverUrl: string, port: number) {
|
||||
const userPrefs = userStore.getPreferences()
|
||||
const envs = {
|
||||
KUBE_CLUSTER_URL: serverUrl,
|
||||
KUBE_CLUSTER_NAME: this.clusterName,
|
||||
KUBERNETES_TLS_SKIP: "true",
|
||||
KUBERNETES_NAMESPACE: this.defaultNamespace,
|
||||
SESSION_SECRET: this.id,
|
||||
LOCAL_SERVER_PORT: port.toString(),
|
||||
KUBE_METRICS_URL: `${serverUrl}/api/v1/namespaces/${this.prometheusPath}/proxy`,
|
||||
STATS_NAMESPACE_DEFAULT: this.prometheusPath.split("/")[0],
|
||||
CHARTS_ENABLED: "true",
|
||||
LENS_VERSION: app.getVersion(),
|
||||
LENS_THEME: `kontena-${userPrefs.colorTheme}`,
|
||||
NODE_ENV: "production",
|
||||
}
|
||||
logger.debug(`spinning up lens-server process with env: ${JSON.stringify(envs)}`)
|
||||
this.localServer = new LensServer(serverUrl, envs)
|
||||
}
|
||||
|
||||
public async ensureServer() {
|
||||
if (!this.localServer) {
|
||||
const currentCluster = this.kc.getCurrentCluster()
|
||||
const clusterUrl = url.parse(currentCluster.server)
|
||||
const serverPort = await this.resolvePort()
|
||||
logger.info(`initializing server for ${clusterUrl.host} on port ${serverPort}`)
|
||||
this.initServer(this.kubernetesApi, serverPort)
|
||||
await this.localServer.run()
|
||||
}
|
||||
if (!this.proxyServer) {
|
||||
const proxyPort = await this.resolveProxyPort()
|
||||
const proxyEnv = Object.assign({}, process.env)
|
||||
@@ -227,10 +162,6 @@ export class ContextHandler {
|
||||
}
|
||||
|
||||
public stopServer() {
|
||||
if (this.localServer) {
|
||||
this.localServer.exit()
|
||||
this.localServer = null
|
||||
}
|
||||
if (this.proxyServer) {
|
||||
this.proxyServer.exit()
|
||||
this.proxyServer = null
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import * as path from "path"
|
||||
import { spawn, ChildProcess } from "child_process"
|
||||
import logger from "./logger"
|
||||
import * as tcpPortUsed from "tcp-port-used"
|
||||
|
||||
declare const __static: string;
|
||||
const isDevelopment = process.env.NODE_ENV !== "production"
|
||||
let serverPath: string = null
|
||||
if (isDevelopment) {
|
||||
serverPath = path.join(process.cwd(), "binaries", "server", process.platform, "lens-server")
|
||||
} else {
|
||||
serverPath = path.join(process.resourcesPath, "lens-server")
|
||||
if (process.platform !== "win32") {
|
||||
serverPath = `${serverPath}.txt`
|
||||
}
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
serverPath = `${serverPath}-${process.arch}.exe`
|
||||
}
|
||||
|
||||
|
||||
export class LensServer {
|
||||
protected serverUrl: string = null
|
||||
protected env: NodeJS.ProcessEnv = null
|
||||
protected localServer: ChildProcess
|
||||
|
||||
constructor(serverUrl: string, env: NodeJS.ProcessEnv) {
|
||||
this.serverUrl = serverUrl
|
||||
this.env = env
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.localServer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
this.localServer = spawn(serverPath, [], {
|
||||
env: this.env,
|
||||
cwd: __static
|
||||
})
|
||||
this.localServer.on("exit", (code) => {
|
||||
logger.error(`server ${this.serverUrl} exited with code ${code}`)
|
||||
this.localServer = null
|
||||
})
|
||||
this.localServer.stdout.on('data', (data) => {
|
||||
logger.debug(`server ${this.serverUrl} stdout: ${data}`)
|
||||
})
|
||||
this.localServer.stderr.on('data', (data) => {
|
||||
logger.debug(`server ${this.serverUrl} stderr: ${data}`)
|
||||
})
|
||||
|
||||
return tcpPortUsed.waitUntilUsed(parseInt(this.env.LOCAL_SERVER_PORT), 500, 10000)
|
||||
}
|
||||
|
||||
public exit() {
|
||||
if (this.localServer) {
|
||||
logger.debug(`Stopping local server: ${this.serverUrl}`)
|
||||
this.localServer.kill()
|
||||
this.localServer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-30
@@ -35,11 +35,7 @@ export class LensProxy {
|
||||
this.handleRequest(proxy, req, res);
|
||||
}.bind(this));
|
||||
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
|
||||
if (this.isRemoteShellRequired(req)) {
|
||||
this.proxyWsUpgrade(proxy, req, socket, head)
|
||||
} else {
|
||||
this.handleWsUpgrade(req, socket, head)
|
||||
}
|
||||
this.handleWsUpgrade(req, socket, head)
|
||||
}.bind(this));
|
||||
|
||||
proxyServer.on("error", (err) => {
|
||||
@@ -135,8 +131,6 @@ export class LensProxy {
|
||||
delete req.headers.authorization
|
||||
req.url = req.url.replace("/api-kube", "")
|
||||
return await contextHandler.getApiTarget()
|
||||
} else {
|
||||
return await contextHandler.getProxyTarget()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,24 +152,13 @@ export class LensProxy {
|
||||
return
|
||||
}
|
||||
contextHandler.ensureServer().then(async () => {
|
||||
if (await this.router.route(cluster, req, res)) return
|
||||
const proxyTarget = await this.getProxyTarget(req, contextHandler)
|
||||
proxy.web(req, res, proxyTarget)
|
||||
})
|
||||
}
|
||||
|
||||
protected async proxyWsUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: Socket, head: Buffer) {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
||||
const contextHandler = cluster.contextHandler
|
||||
contextHandler.applyHeaders(req);
|
||||
const reqUrl = url.parse(req.url, true)
|
||||
const urlParams = reqUrl.query
|
||||
for (const [key, value] of Object.entries(urlParams)) {
|
||||
if (key !== "token") {
|
||||
req.headers["x-lens-param-" + key] = value
|
||||
if (proxyTarget) {
|
||||
proxy.web(req, res, proxyTarget)
|
||||
} else {
|
||||
await this.router.route(cluster, req, res)
|
||||
}
|
||||
}
|
||||
proxy.ws(req, socket, head, await contextHandler.getProxyTarget());
|
||||
})
|
||||
}
|
||||
|
||||
protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) {
|
||||
@@ -187,13 +170,6 @@ export class LensProxy {
|
||||
wsServer.emit("connection", con, req);
|
||||
});
|
||||
}
|
||||
|
||||
protected isRemoteShellRequired(req: http.IncomingMessage) {
|
||||
if (!LensProxy.localShellSessions) {
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function listen(port: number, clusterManager: ClusterManager) {
|
||||
|
||||
+40
-2
@@ -1,13 +1,23 @@
|
||||
import * as http from "http";
|
||||
import { Cluster } from "./cluster";
|
||||
import * as http from "http"
|
||||
import * as path from "path"
|
||||
import { Cluster } from "./cluster"
|
||||
import { configRoute } from "./routes/config"
|
||||
import { helmApi } from "./helm-api"
|
||||
import { resourceApplierApi } from "./resource-applier-api"
|
||||
import { kubeconfigRoute } from "./routes/kubeconfig"
|
||||
import { metricsRoute } from "./routes/metrics"
|
||||
import { watchRoute } from "./routes/watch"
|
||||
import { readFile } from "fs"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Call = require('@hapi/call');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Subtext = require('@hapi/subtext');
|
||||
|
||||
declare const __static: string;
|
||||
|
||||
const assetsPath = path.join(__static, "build/client")
|
||||
|
||||
interface RouteParams {
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
@@ -64,7 +74,35 @@ export class Router {
|
||||
return request
|
||||
}
|
||||
|
||||
protected handleStaticFile(file: string, response: http.ServerResponse) {
|
||||
const asset = path.join(assetsPath, file)
|
||||
readFile(asset, (err, data) => {
|
||||
if (err) {
|
||||
response.statusCode = 404
|
||||
} else {
|
||||
response.write(data)
|
||||
response.end()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected addRoutes() {
|
||||
// Static assets
|
||||
this.router.add({ method: 'get', path: '/{path*}' }, (request: LensApiRequest) => {
|
||||
const { response, params } = request
|
||||
const file = params.path || "/index.html"
|
||||
this.handleStaticFile(file, response)
|
||||
})
|
||||
|
||||
this.router.add({ method: 'get', path: '/api/config' }, configRoute.routeConfig.bind(configRoute))
|
||||
this.router.add({ method: 'get', path: '/api/kubeconfig/service-account/{namespace}/{account}' }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
|
||||
|
||||
// Watch API
|
||||
this.router.add({ method: 'get', path: '/api/watch' }, watchRoute.routeWatch.bind(watchRoute))
|
||||
|
||||
// Metrics API
|
||||
this.router.add({ method: 'post', path: '/api/metrics' }, metricsRoute.routeMetrics.bind(metricsRoute))
|
||||
|
||||
// Helm API
|
||||
this.router.add({ method: 'get', path: '/api-helm/v2/charts' }, helmApi.listCharts.bind(helmApi))
|
||||
this.router.add({ method: 'get', path: '/api-helm/v2/charts/{repo}/{chart}' }, helmApi.getChart.bind(helmApi))
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { userStore } from "../../common/user-store"
|
||||
import { getAppVersion } from "../../common/app-utils"
|
||||
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
|
||||
import { Cluster } from "../cluster"
|
||||
|
||||
|
||||
function selfSubjectAccessReview(authApi: AuthorizationV1Api, namespace: string) {
|
||||
return authApi.createSelfSubjectAccessReview({
|
||||
apiVersion: "authorization.k8s.io/v1",
|
||||
kind: "SelfSubjectAccessReview",
|
||||
spec: {
|
||||
resourceAttributes: {
|
||||
namespace: namespace,
|
||||
resource: "pod",
|
||||
verb: "list",
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getAllowedNamespaces(cluster: Cluster) {
|
||||
const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api)
|
||||
const authApi = cluster.contextHandler.kc.makeApiClient(AuthorizationV1Api)
|
||||
try {
|
||||
const namespaceList = await api.listNamespace()
|
||||
const nsAccessStatuses = await Promise.all(
|
||||
namespaceList.body.items.map(ns => {
|
||||
return selfSubjectAccessReview(authApi, ns.metadata.name)
|
||||
})
|
||||
)
|
||||
return namespaceList.body.items
|
||||
.filter((ns, i) => nsAccessStatuses[i].body.status.allowed)
|
||||
.map(ns => ns.metadata.name)
|
||||
} catch(error) {
|
||||
const kc = cluster.contextHandler.kc
|
||||
const ctx = kc.getContextObject(kc.currentContext)
|
||||
if (ctx.namespace) {
|
||||
return [ctx.namespace]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigRoute extends LensApi {
|
||||
|
||||
public async routeConfig(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
|
||||
const data = {
|
||||
clusterName: cluster.contextName,
|
||||
lensVersion: getAppVersion(),
|
||||
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
|
||||
kubeVersion: cluster.version,
|
||||
chartsEnabled: true,
|
||||
isClusterAdmin: cluster.isAdmin,
|
||||
allowedNamespaces: await getAllowedNamespaces(cluster)
|
||||
};
|
||||
|
||||
this.respondJson(response, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const configRoute = new ConfigRoute()
|
||||
@@ -0,0 +1,58 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { Cluster } from "../cluster"
|
||||
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
|
||||
|
||||
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
|
||||
const tokenData = new Buffer(secret.data["token"], "base64")
|
||||
return {
|
||||
'apiVersion': 'v1',
|
||||
'kind': 'Config',
|
||||
'clusters': [
|
||||
{
|
||||
'name': cluster.contextName,
|
||||
'cluster': {
|
||||
'server': cluster.contextHandler.kc.getCurrentCluster().server,
|
||||
'certificate-authority-data': secret.data["ca.crt"]
|
||||
}
|
||||
}
|
||||
],
|
||||
'users': [
|
||||
{
|
||||
'name': username,
|
||||
'user': {
|
||||
'token': tokenData.toString("utf8"),
|
||||
}
|
||||
}
|
||||
],
|
||||
'contexts': [
|
||||
{
|
||||
'name': cluster.contextName,
|
||||
'context': {
|
||||
'user': username,
|
||||
'cluster': cluster.contextName,
|
||||
'namespace': secret.metadata.namespace,
|
||||
}
|
||||
}
|
||||
],
|
||||
'current-context': cluster.contextName
|
||||
}
|
||||
}
|
||||
|
||||
class KubeconfigRoute extends LensApi {
|
||||
|
||||
public async routeServiceAccountRoute(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
|
||||
const client = cluster.contextHandler.kc.makeApiClient(CoreV1Api);
|
||||
const secretList = await client.listNamespacedSecret(params.namespace)
|
||||
const secret = secretList.body.items.find(secret => {
|
||||
const { annotations } = secret.metadata;
|
||||
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
|
||||
});
|
||||
const data = generateKubeConfig(params.account, secret, cluster);
|
||||
this.respondJson(response, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const kubeconfigRoute = new KubeconfigRoute()
|
||||
@@ -0,0 +1,75 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import * as requestPromise from "request-promise-native"
|
||||
|
||||
type MetricsQuery = string | string[] | {
|
||||
[metricName: string]: string;
|
||||
}
|
||||
|
||||
class MetricsRoute extends LensApi {
|
||||
|
||||
public async routeMetrics(request: LensApiRequest) {
|
||||
const { response, cluster} = request
|
||||
const query: MetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
|
||||
const metricsUrl = `${serverUrl}/api/v1/namespaces/${cluster.contextHandler.getPrometheusPath()}/proxy/api/v1/query_range`
|
||||
const headers = {
|
||||
"Host": `${cluster.id}.localhost:${cluster.port}`,
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
const queryParams: MetricsQuery = {}
|
||||
request.query.forEach((value: string, key: string) => {
|
||||
queryParams[key] = value
|
||||
})
|
||||
|
||||
// prometheus metrics loader
|
||||
const attempts: { [query: string]: number } = {};
|
||||
const maxAttempts = 5;
|
||||
const loadMetrics = (orgQuery: string): Promise<any> => {
|
||||
const query = orgQuery.trim()
|
||||
const attempt = attempts[query] = (attempts[query] || 0) + 1;
|
||||
return requestPromise(metricsUrl, {
|
||||
resolveWithFullResponse: false,
|
||||
headers: headers,
|
||||
json: true,
|
||||
qs: {
|
||||
query: query,
|
||||
...queryParams
|
||||
}
|
||||
}).catch(async (error) => {
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
|
||||
return loadMetrics(query);
|
||||
}
|
||||
return {
|
||||
status: error.toString(),
|
||||
data: {
|
||||
result: []
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// return data in same structure as query
|
||||
let data: any;
|
||||
if (typeof query === "string") {
|
||||
data = await loadMetrics(query)
|
||||
}
|
||||
else if (Array.isArray(query)) {
|
||||
data = await Promise.all(query.map(loadMetrics));
|
||||
}
|
||||
else {
|
||||
data = {};
|
||||
const result = await Promise.all(
|
||||
Object.values(query).map(loadMetrics)
|
||||
);
|
||||
Object.keys(query).forEach((metricName, index) => {
|
||||
data[metricName] = result[index];
|
||||
});
|
||||
}
|
||||
|
||||
this.respondJson(response, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const metricsRoute = new MetricsRoute()
|
||||
@@ -0,0 +1,108 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
|
||||
import { ServerResponse } from "http"
|
||||
import { Request } from "request"
|
||||
import logger from "../logger"
|
||||
|
||||
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 start() {
|
||||
if (this.processor) {
|
||||
clearInterval(this.processor)
|
||||
}
|
||||
this.processor = setInterval(() => {
|
||||
const events = this.eventBuffer.splice(0)
|
||||
events.map(event => this.sendEvent(event))
|
||||
this.response.flushHeaders()
|
||||
}, 50)
|
||||
this.watchRequest = 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)
|
||||
this.watchRequest.abort()
|
||||
}
|
||||
|
||||
private watchHandler(phase: string, obj: RuntimeRawExtension) {
|
||||
this.eventBuffer.push({
|
||||
type: phase,
|
||||
object: obj
|
||||
})
|
||||
}
|
||||
|
||||
private doneHandler(error: Error) {
|
||||
if (error) {
|
||||
logger.error("watch error: " + error.toString())
|
||||
this.sendEvent({
|
||||
type: "STREAM_END",
|
||||
url: this.apiUrl,
|
||||
status: 410,
|
||||
})
|
||||
return
|
||||
}
|
||||
this.start()
|
||||
}
|
||||
|
||||
private sendEvent(evt: any) {
|
||||
// convert to "text/event-stream" format
|
||||
this.response.write(`data: ${JSON.stringify(evt)}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
class WatchRoute extends LensApi {
|
||||
|
||||
public async routeWatch(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
const apis: string[] = request.query.getAll("api")
|
||||
const watchers: ApiWatcher[] = []
|
||||
|
||||
if (!apis.length) {
|
||||
this.respondJson(response, {
|
||||
message: "Empty request. Query params 'api' are not provided.",
|
||||
example: "?api=/api/v1/pods&api=/api/v1/nodes",
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
|
||||
response.setHeader("Content-Type", "text/event-stream")
|
||||
response.setHeader("Cache-Control", "no-cache")
|
||||
response.setHeader("Connection", "keep-alive")
|
||||
|
||||
apis.forEach(apiUrl => {
|
||||
const watcher = new ApiWatcher(apiUrl, cluster.contextHandler.kc, response)
|
||||
watcher.start()
|
||||
watchers.push(watcher)
|
||||
})
|
||||
|
||||
request.raw.req.on("close", () => {
|
||||
logger.debug("Watch request closed")
|
||||
watchers.map(watcher => watcher.stop())
|
||||
})
|
||||
|
||||
request.raw.req.on("end", () => {
|
||||
logger.debug("Watch request ended")
|
||||
watchers.map(watcher => watcher.stop())
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const watchRoute = new WatchRoute()
|
||||
Reference in new issue
Block a user