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

clean up / fixes

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-11 13:17:18 +03:00
parent e72e28ddab
commit 5eca8bc01c
19 changed files with 168 additions and 199 deletions

View File

@ -18,7 +18,7 @@ export interface ClusterModel {
kubeConfigPath: string; kubeConfigPath: string;
/** @deprecated */ /** @deprecated */
kubeConfig?: string; // kube-config yaml kubeConfig?: string; // yaml
} }
export interface ClusterPreferences { export interface ClusterPreferences {
@ -48,7 +48,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}); });
} }
@observable activeCluster: ClusterId; // todo: use active "context" from kube-config? @observable activeCluster: ClusterId; // todo: current-context from kube-config?
@observable removedClusters = observable.map<ClusterId, Cluster>(); @observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>(); @observable clusters = observable.map<ClusterId, Cluster>();

View File

@ -15,7 +15,7 @@ export interface IpcMessageHandler {
(...args: any[]): any; (...args: any[]): any;
} }
export function sendMessageToRenderer(channel: IpcChannel, ...args: any[]) { export function sendMessage(channel: IpcChannel, ...args: any[]) {
webContents.getFocusedWebContents().send(channel, ...args); webContents.getFocusedWebContents().send(channel, ...args);
} }

View File

@ -10,10 +10,10 @@ export const isDevelopment = isDebugging || !isProduction;
export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}` export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`
export const appProto = "lens" // app's "userData" folder (e.g. "lens://icons/logo.svg") export const appProto = "lens" // app.getPath("userData") folder
export const staticProto = "static" // static content folder (e.g. "static://RELEASE_NOTES.md") export const staticProto = "static" // static folder (e.g. "static://RELEASE_NOTES.md")
// Paths // System paths
export const contextDir = process.cwd(); export const contextDir = process.cwd();
export const staticDir = path.join(contextDir, "static"); export const staticDir = path.join(contextDir, "static");
export const outDir = path.join(contextDir, "out"); export const outDir = path.join(contextDir, "out");
@ -23,12 +23,8 @@ export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
// Apis // Apis
export const apiPrefix = { export const apiPrefix = "/api-local" // local router apis
BASE: '/api', export const apiKubePrefix = "/api-kube" // k8s cluster apis
KUBE_BASE: '/api-kube', // kubernetes cluster api
KUBE_HELM: '/api-helm', // helm charts api
KUBE_RESOURCE_APPLIER: "/api-resource",
};
// Links // Links
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues"

View File

@ -4,7 +4,7 @@ import path from "path"
import http from "http" import http from "http"
import { copyFile, ensureDir } from "fs-extra" import { copyFile, ensureDir } from "fs-extra"
import filenamify from "filenamify" import filenamify from "filenamify"
import { apiPrefix, appProto } from "../common/vars"; import { apiKubePrefix, appProto } from "../common/vars";
import { ClusterId, ClusterModel, clusterStore } from "../common/cluster-store" import { ClusterId, ClusterModel, clusterStore } from "../common/cluster-store"
import { handleMessages } from "../common/ipc-helpers"; import { handleMessages } from "../common/ipc-helpers";
import { ClusterIpcMessage } from "../common/ipc-messages"; import { ClusterIpcMessage } from "../common/ipc-messages";
@ -100,7 +100,7 @@ export class ClusterManager {
cluster = this.getCluster(clusterId) cluster = this.getCluster(clusterId)
if (cluster) { if (cluster) {
// we need to swap path prefix so that request is proxied to kube api // we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiPrefix.KUBE_BASE) req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
} }
} }
} else { } else {

View File

@ -37,8 +37,8 @@ export class Cluster implements ClusterModel {
protected kubeconfigManager: KubeconfigManager; protected kubeconfigManager: KubeconfigManager;
@observable initialized = false; @observable initialized = false;
@observable workspace: string;
@observable contextName: string; @observable contextName: string;
@observable workspace: string;
@observable kubeConfigPath: string; @observable kubeConfigPath: string;
@observable port: number; @observable port: number;
@observable url: string; // cluster-api url @observable url: string; // cluster-api url
@ -70,14 +70,15 @@ export class Cluster implements ClusterModel {
@action @action
async init() { async init() {
try { try {
// fixme: all broken // fixme
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
this.port = await this.contextHandler.resolveProxyPort(); // resolve port before KubeconfigManager this.port = await this.contextHandler.ensurePort(); // resolve port before KubeconfigManager
this.webContentUrl = `http://${this.id}.localhost:${this.port}`;
this.kubeAuthProxyUrl = `http://127.0.0.1:${this.port}`; this.kubeAuthProxyUrl = `http://127.0.0.1:${this.port}`;
this.kubeconfigManager = new KubeconfigManager(this); this.kubeconfigManager = new KubeconfigManager(this);
// this.url = this.kubeconfigManager.getCurrentClusterServer(); // this.url = this.kubeconfigManager.getCurrentClusterServer();
// this.apiUrl = url.parse(this.url); // this.apiUrl = url.parse(this.url);
this.webContentUrl = `http://${this.id}.localhost:${this.port}`;
logger.info(`[CLUSTER]: init success`, { logger.info(`[CLUSTER]: init success`, {
id: this.id, id: this.id,

View File

@ -1,18 +1,19 @@
import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry" import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
import type { ClusterPreferences } from "../common/cluster-store"; import type { ClusterPreferences } from "../common/cluster-store";
import type { ServerOptions } from "http-proxy"
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import type httpProxy from "http-proxy"
import { CoreV1Api } from "@kubernetes/client-node" import { CoreV1Api } from "@kubernetes/client-node"
import { observable } from "mobx";
import { prometheusProviders } from "../common/prometheus-providers" import { prometheusProviders } from "../common/prometheus-providers"
import logger from "./logger" import logger from "./logger"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy" import { KubeAuthProxy } from "./kube-auth-proxy"
export class ContextHandler { export class ContextHandler {
public proxyPort: number @observable proxyPort: number;
protected proxyServer: KubeAuthProxy protected proxyServer: KubeAuthProxy
protected apiTarget: ServerOptions protected apiTarget: httpProxy.ServerOptions
protected prometheusProvider: string protected prometheusProvider: string
protected prometheusPath: string protected prometheusPath: string
@ -44,7 +45,7 @@ export class ContextHandler {
} }
public async getPrometheusService(): Promise<PrometheusService> { public async getPrometheusService(): Promise<PrometheusService> {
const providers = this.prometheusProvider ? prometheusProviders.filter((p, _) => p.id == this.prometheusProvider) : prometheusProviders const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => { const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
const apiClient = this.cluster.proxyKubeconfig().makeApiClient(CoreV1Api) const apiClient = this.cluster.proxyKubeconfig().makeApiClient(CoreV1Api)
return await provider.getPrometheusService(apiClient) return await provider.getPrometheusService(apiClient)
@ -66,7 +67,7 @@ export class ContextHandler {
return this.prometheusPath; return this.prometheusPath;
} }
public async getApiTarget(isWatchRequest = false): Promise<ServerOptions> { public async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
if (this.apiTarget && !isWatchRequest) { if (this.apiTarget && !isWatchRequest) {
return this.apiTarget return this.apiTarget
} }
@ -78,24 +79,24 @@ export class ContextHandler {
return apiTarget return apiTarget
} }
// fixme public async getApiTargetUrl(): Promise<string> {
protected async newApiTarget(timeout: number): Promise<ServerOptions> { const port = await this.ensurePort();
const { path } = this.cluster.apiUrl;
return `http://127.0.0.1:${port}${path}`;
}
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
return { return {
changeOrigin: true, changeOrigin: true,
target: await this.getApiTargetUrl(),
timeout: timeout, timeout: timeout,
headers: { headers: {
"Host": this.cluster.apiUrl.hostname "Host": this.cluster.apiUrl.hostname,
}, }
target: {
port: await this.resolveProxyPort(),
protocol: "http://",
host: "localhost",
path: this.cluster.apiUrl.path,
},
} }
} }
async resolveProxyPort(): Promise<number> { async ensurePort(): Promise<number> {
if (!this.proxyPort) { if (!this.proxyPort) {
this.proxyPort = await getFreePort(); this.proxyPort = await getFreePort();
} }
@ -104,7 +105,7 @@ export class ContextHandler {
public async ensureServer() { public async ensureServer() {
if (!this.proxyServer) { if (!this.proxyServer) {
await this.resolveProxyPort(); await this.ensurePort();
const proxyEnv = Object.assign({}, process.env) const proxyEnv = Object.assign({}, process.env)
if (this.cluster.preferences.httpsProxy) { if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy

View File

@ -1,6 +1,6 @@
import { ChildProcess, spawn } from "child_process" import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { sendMessageToRenderer } from "../common/ipc-helpers"; import { sendMessage } from "../common/ipc-helpers";
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl" import { bundledKubectl, Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
@ -84,7 +84,7 @@ export class KubeAuthProxy {
const channel = `kube-auth:${this.cluster.id}` const channel = `kube-auth:${this.cluster.id}`
const message = { data, stream }; const message = { data, stream };
logger.debug(channel, message); logger.debug(channel, message);
sendMessageToRenderer(channel, message); sendMessage(channel, message);
} }
public exit() { public exit() {

View File

@ -10,7 +10,11 @@ export class KubeconfigManager {
protected tempFile: string protected tempFile: string
constructor(protected cluster: Cluster) { constructor(protected cluster: Cluster) {
this.tempFile = this.createTemporaryKubeconfig(); this.init();
}
protected async init() {
this.tempFile = await this.createTemporaryKubeconfig();
} }
getPath() { getPath() {
@ -21,7 +25,7 @@ export class KubeconfigManager {
* Creates new "temporary" kubeconfig that point to the kubectl-proxy. * Creates new "temporary" kubeconfig that point to the kubectl-proxy.
* This way any user of the config does not need to know anything about the auth etc. details. * This way any user of the config does not need to know anything about the auth etc. details.
*/ */
protected createTemporaryKubeconfig(): string { protected async createTemporaryKubeconfig(): Promise<string> {
fs.ensureDir(this.configDir); fs.ensureDir(this.configDir);
const path = `${this.configDir}/${randomFileName("kubeconfig")}`; const path = `${this.configDir}/${randomFileName("kubeconfig")}`;
const { contextName, contextHandler, kubeConfigPath } = this.cluster; const { contextName, contextHandler, kubeConfigPath } = this.cluster;
@ -29,7 +33,7 @@ export class KubeconfigManager {
kubeConfig.clusters = [ kubeConfig.clusters = [
{ {
name: contextName, name: contextName,
server: `http://127.0.0.1:${contextHandler.proxyPort}`, // fixme: extract server: await contextHandler.getApiTargetUrl(),
skipTLSVerify: true, skipTLSVerify: true,
} }
]; ];
@ -42,7 +46,7 @@ export class KubeconfigManager {
user: "proxy", user: "proxy",
name: contextName, name: contextName,
cluster: contextName, cluster: contextName,
// namespace: kubeConfig.getContextObject(contextName).namespace, namespace: kubeConfig.getContextObject(contextName).namespace,
} }
]; ];
logger.info(`Creating temp config for context "${contextName}" at "${path}"`); logger.info(`Creating temp config for context "${contextName}" at "${path}"`);

View File

@ -1,13 +1,13 @@
import net from "net";
import http from "http"; import http from "http";
import httpProxy from "http-proxy"; import httpProxy from "http-proxy";
import { Socket } from "net"; import url from "url";
import * as url from "url";
import * as WebSocket from "ws" import * as WebSocket from "ws"
import { ContextHandler } from "./context-handler";
import * as nodeShell from "./node-shell-session" import * as nodeShell from "./node-shell-session"
import { ClusterManager } from "./cluster-manager"
import { Router } from "./router" import { Router } from "./router"
import { apiPrefix } from "../common/vars"; import { ClusterManager } from "./cluster-manager"
import { ContextHandler } from "./context-handler";
import { apiKubePrefix } from "../common/vars";
import logger from "./logger" import logger from "./logger"
export class LensProxy { export class LensProxy {
@ -27,7 +27,7 @@ export class LensProxy {
} }
listen(): this { listen(): this {
const proxyServer = this.buildProxyServer(); const proxyServer = this.buildCustomProxy();
const { proxyPort } = this.clusterManager; const { proxyPort } = this.clusterManager;
this.proxyServer = proxyServer.listen(proxyPort); this.proxyServer = proxyServer.listen(proxyPort);
logger.info(`LensProxy server has started http://localhost:${proxyPort}`); logger.info(`LensProxy server has started http://localhost:${proxyPort}`);
@ -40,21 +40,21 @@ export class LensProxy {
this.closed = true this.closed = true
} }
protected buildProxyServer() { protected buildCustomProxy(): http.Server {
const proxy = this.createProxy(); const proxy = this.createProxy();
const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { const customProxy = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res); this.handleRequest(proxy, req, res);
}); });
proxyServer.on("upgrade", (req: http.IncomingMessage, socket: Socket, head: Buffer) => { customProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
this.handleWsUpgrade(req, socket, head) this.handleWsUpgrade(req, socket, head)
}); });
proxyServer.on("error", (err) => { customProxy.on("error", (err) => {
logger.error("proxy error", err) logger.error("proxy error", err)
}); });
return proxyServer; return customProxy;
} }
protected createProxy() { protected createProxy(): httpProxy {
const proxy = httpProxy.createProxyServer(); const proxy = httpProxy.createProxyServer();
proxy.on("proxyRes", (proxyRes, req, res) => { proxy.on("proxyRes", (proxyRes, req, res) => {
@ -71,16 +71,18 @@ export class LensProxy {
if (req.method !== "GET") { if (req.method !== "GET") {
return return
} }
const key = `${req.headers.host}${req.url}` const reqUrl = `${req.headers.host}${req.url}`
if (this.retryCounters.has(key)) { if (this.retryCounters.has(reqUrl)) {
logger.debug("Resetting proxy retry cache for url: " + key) logger.debug("Resetting proxy retry cache for url: " + reqUrl)
this.retryCounters.delete(key) this.retryCounters.delete(reqUrl)
} }
}) })
proxy.on("error", (error, req, res, target) => { proxy.on("error", (error, req, res, target) => {
if (this.closed) return; if (this.closed) {
return;
}
if (target) { if (target) {
logger.debug("Failed proxy to target: " + JSON.stringify(target)) logger.debug("Failed proxy to target: " + JSON.stringify(target, null, 2));
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
const retryCounterKey = `${req.headers.host}${req.url}` const retryCounterKey = `${req.headers.host}${req.url}`
const retryCount = this.retryCounters.get(retryCounterKey) || 0 const retryCount = this.retryCounters.get(retryCounterKey) || 0
@ -112,11 +114,11 @@ export class LensProxy {
})); }));
} }
// fixme: remove api prefix?
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> { protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
const prefix = apiPrefix.KUBE_BASE; if (req.url.startsWith(apiKubePrefix)) {
if (req.url.startsWith(prefix)) {
delete req.headers.authorization delete req.headers.authorization
req.url = req.url.replace(prefix, "") req.url = req.url.replace(apiKubePrefix, "")
const isWatchRequest = req.url.includes("watch=") const isWatchRequest = req.url.includes("watch=")
return await contextHandler.getApiTarget(isWatchRequest) return await contextHandler.getApiTarget(isWatchRequest)
} }
@ -137,11 +139,11 @@ export class LensProxy {
if (proxyTarget) { if (proxyTarget) {
proxy.web(req, res, proxyTarget) proxy.web(req, res, proxyTarget)
} else { } else {
this.router.route(cluster, req, res); // todo: handle "not-found" if isBoom==true? this.router.route(cluster, req, res);
} }
} }
protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) { protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const wsServer = this.createWsListener(); const wsServer = this.createWsListener();
wsServer.handleUpgrade(req, socket, head, (con) => { wsServer.handleUpgrade(req, socket, head, (con) => {
wsServer.emit("connection", con, req); wsServer.emit("connection", con, req);

View File

@ -1,7 +1,7 @@
import logger from "./logger" import logger from "./logger"
import { createServer, AddressInfo } from "net" import { createServer, AddressInfo } from "net"
// todo: replace with https://github.com/http-party/node-portfinder ? // todo: use https://github.com/http-party/node-portfinder ?
const getNextAvailablePort = () => { const getNextAvailablePort = () => {
logger.debug("getNextAvailablePort() start") logger.debug("getNextAvailablePort() start")

View File

@ -9,37 +9,28 @@ import { resourceApplierApi } from "./resource-applier-api"
import { apiPrefix, appName, outDir } from "../common/vars"; import { apiPrefix, appName, outDir } from "../common/vars";
import { configRoute, kubeconfigRoute, metricsRoute, portForwardRoute, watchRoute } from "./routes"; import { configRoute, kubeconfigRoute, metricsRoute, portForwardRoute, watchRoute } from "./routes";
const mimeTypes: Record<string, string> = { export interface RouterRequestOpts<P extends Record<string, string> = any> {
"html": "text/html", req: http.IncomingMessage;
"txt": "text/plain", res: http.ServerResponse;
"css": "text/css", cluster: Cluster;
"gif": "image/gif", url: URL;
"jpg": "image/jpeg", params: P; // https://hapi.dev/module/call/api
"png": "image/png",
"svg": "image/svg+xml",
"js": "application/javascript",
"woff2": "font/woff2",
"ttf": "font/ttf"
};
interface RouteParams {
[key: string]: string | undefined;
} }
export type LensApiRequest = { export interface LensApiRequest<D = any, P = any> {
path: string;
payload: D;
params: P;
cluster: Cluster; cluster: Cluster;
payload: any;
raw: {
req: http.IncomingMessage;
};
params: RouteParams;
response: http.ServerResponse; response: http.ServerResponse;
query: URLSearchParams; query: URLSearchParams;
path: string; raw: {
req: http.IncomingMessage;
}
} }
export class Router { export class Router {
protected router: any protected router: any;
public constructor() { public constructor() {
this.router = new Call.Router(); this.router = new Call.Router();
@ -47,20 +38,23 @@ export class Router {
} }
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> { public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
const reqUrl = new URL(req.url, "http://localhost"); const url = new URL(req.url, "http://localhost");
const path = reqUrl.pathname const path = url.pathname
const method = req.method.toLowerCase() const method = req.method.toLowerCase()
const matchingRoute = this.router.route(method, path); const matchingRoute = this.router.route(method, path);
const routeExists = !matchingRoute.isBoom; const routeFound = !matchingRoute.isBoom;
if (routeExists) { if (routeFound) {
const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params }) const request = await this.getRequest({
req, res, cluster, url,
params: matchingRoute.params
});
await matchingRoute.route(request) await matchingRoute.route(request)
return true return true
} }
return false; return false;
} }
protected async getRequest(opts: { req: http.IncomingMessage; res: http.ServerResponse; cluster: Cluster; url: URL; params: RouteParams }) { protected async getRequest(opts: RouterRequestOpts) {
const { req, res, url, cluster, params } = opts const { req, res, url, cluster, params } = opts
const { payload } = await Subtext.parse(req, null, { parse: true, output: 'data' }); const { payload } = await Subtext.parse(req, null, { parse: true, output: 'data' });
const request: LensApiRequest = { const request: LensApiRequest = {
@ -78,6 +72,18 @@ export class Router {
} }
protected getMimeType(filename: string) { protected getMimeType(filename: string) {
const mimeTypes: Record<string, string> = {
html: "text/html",
txt: "text/plain",
css: "text/css",
gif: "image/gif",
jpg: "image/jpeg",
png: "image/png",
svg: "image/svg+xml",
js: "application/javascript",
woff2: "font/woff2",
ttf: "font/ttf"
};
return mimeTypes[path.extname(filename).slice(1)] || "text/plain" return mimeTypes[path.extname(filename).slice(1)] || "text/plain"
} }
@ -94,12 +100,6 @@ export class Router {
} }
protected addRoutes() { protected addRoutes() {
const {
BASE: apiBase,
KUBE_HELM: apiHelm,
KUBE_RESOURCE_APPLIER: apiResource,
} = apiPrefix;
// Static assets // Static assets
this.router.add({ method: 'get', path: '/{path*}' }, (request: LensApiRequest) => { this.router.add({ method: 'get', path: '/{path*}' }, (request: LensApiRequest) => {
const { response, params } = request const { response, params } = request
@ -107,33 +107,33 @@ export class Router {
this.handleStaticFile(file, response) this.handleStaticFile(file, response)
}) })
this.router.add({ method: "get", path: `${apiBase}/config` }, configRoute.routeConfig.bind(configRoute)) this.router.add({ method: "get", path: `${apiPrefix}/config` }, configRoute.routeConfig.bind(configRoute))
this.router.add({ method: "get", path: `${apiBase}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute)) this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
// Watch API // Watch API
this.router.add({ method: "get", path: `${apiBase}/watch` }, watchRoute.routeWatch.bind(watchRoute)) this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute))
// Metrics API // Metrics API
this.router.add({ method: "post", path: `${apiBase}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)) this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute))
// Port-forward API // Port-forward API
this.router.add({ method: "post", path: `${apiBase}/services/{namespace}/{service}/port-forward/{port}` }, portForwardRoute.routeServicePortForward.bind(portForwardRoute)) this.router.add({ method: "post", path: `${apiPrefix}/services/{namespace}/{service}/port-forward/{port}` }, portForwardRoute.routeServicePortForward.bind(portForwardRoute))
// Helm API // Helm API
this.router.add({ method: "get", path: `${apiHelm}/v2/charts` }, helmApi.listCharts.bind(helmApi)) this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmApi.listCharts.bind(helmApi))
this.router.add({ method: "get", path: `${apiHelm}/v2/charts/{repo}/{chart}` }, helmApi.getChart.bind(helmApi)) this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmApi.getChart.bind(helmApi))
this.router.add({ method: "get", path: `${apiHelm}/v2/charts/{repo}/{chart}/values` }, helmApi.getChartValues.bind(helmApi)) this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmApi.getChartValues.bind(helmApi))
this.router.add({ method: "post", path: `${apiHelm}/v2/releases` }, helmApi.installChart.bind(helmApi)) this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmApi.installChart.bind(helmApi))
this.router.add({ method: `put`, path: `${apiHelm}/v2/releases/{namespace}/{release}` }, helmApi.updateRelease.bind(helmApi)) this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmApi.updateRelease.bind(helmApi))
this.router.add({ method: `put`, path: `${apiHelm}/v2/releases/{namespace}/{release}/rollback` }, helmApi.rollbackRelease.bind(helmApi)) this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmApi.rollbackRelease.bind(helmApi))
this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace?}` }, helmApi.listReleases.bind(helmApi)) this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmApi.listReleases.bind(helmApi))
this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace}/{release}` }, helmApi.getRelease.bind(helmApi)) this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmApi.getRelease.bind(helmApi))
this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace}/{release}/values` }, helmApi.getReleaseValues.bind(helmApi)) this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmApi.getReleaseValues.bind(helmApi))
this.router.add({ method: "get", path: `${apiHelm}/v2/releases/{namespace}/{release}/history` }, helmApi.getReleaseHistory.bind(helmApi)) this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmApi.getReleaseHistory.bind(helmApi))
this.router.add({ method: "delete", path: `${apiHelm}/v2/releases/{namespace}/{release}` }, helmApi.deleteRelease.bind(helmApi)) this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmApi.deleteRelease.bind(helmApi))
// Resource Applier API // Resource Applier API
this.router.add({ method: "post", path: `${apiResource}/stack` }, resourceApplierApi.applyResource.bind(resourceApplierApi)) this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierApi.applyResource.bind(resourceApplierApi))
} }
} }

View File

@ -2,55 +2,41 @@ import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import requestPromise from "request-promise-native" import requestPromise from "request-promise-native"
import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry" import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry"
import { apiPrefix } from "../../common/vars";
export type IMetricsQuery = string | string[] | { export type IMetricsQuery = string | string[] | {
[metricName: string]: string; [metricName: string]: string;
} }
class MetricsRoute extends LensApi { class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest<IMetricsQuery>) {
public async routeMetrics(request: LensApiRequest) { const { response, cluster, payload } = request
const { response, cluster } = request const { contextHandler } = cluster;
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}` // fixme: extract const serverUrl = await contextHandler.getApiTargetUrl();
const query: IMetricsQuery = request.payload;
const headers: Record<string, string> = {
"Host": `${cluster.id}.localhost:${cluster.port}`,
"Content-type": "application/json",
}
const queryParams: IMetricsQuery = {}
request.query.forEach((value: string, key: string) => {
queryParams[key] = value
})
let metricsUrl: string let metricsUrl: string
let prometheusProvider: PrometheusProvider let prometheusProvider: PrometheusProvider
try { try {
const prometheusPath = await cluster.contextHandler.getPrometheusPath() const prometheusPath = await contextHandler.getPrometheusPath()
metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
prometheusProvider = await cluster.contextHandler.getPrometheusProvider() prometheusProvider = await contextHandler.getPrometheusProvider()
} catch { } catch {
this.respondJson(response, {}) this.respondJson(response, {})
return return
} }
// prometheus metrics loader // prometheus metrics loader
const attempts: { [query: string]: number } = {}; const attempts: Record<string, number> = {};
const maxAttempts = 5; const maxAttempts = 5;
const loadMetrics = (orgQuery: string): Promise<any> => { const loadMetrics = (promQuery: string): Promise<any> => {
const query = orgQuery.trim() const queryString = request.query.toString() + `&query=` + promQuery;
const attempt = attempts[query] = (attempts[query] || 0) + 1; const attempt = attempts[queryString] = (attempts[queryString] || 0) + 1;
return requestPromise(metricsUrl, { return requestPromise(metricsUrl, {
resolveWithFullResponse: false,
headers: headers,
json: true, json: true,
qs: { qs: queryString,
query: query, useQuerystring: true,
...queryParams resolveWithFullResponse: false,
}
}).catch(async (error) => { }).catch(async (error) => {
if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) { if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
return loadMetrics(query); return loadMetrics(queryString);
} }
return { return {
status: error.toString(), status: error.toString(),
@ -63,14 +49,14 @@ class MetricsRoute extends LensApi {
// return data in same structure as query // return data in same structure as query
let data: any; let data: any;
if (typeof query === "string") { if (typeof payload === "string") {
data = await loadMetrics(query) data = await loadMetrics(payload)
} else if (Array.isArray(query)) { } else if (Array.isArray(payload)) {
data = await Promise.all(query.map(loadMetrics)); data = await Promise.all(payload.map(loadMetrics));
} else { } else {
data = {}; data = {};
const result = await Promise.all( const result = await Promise.all(
Object.entries(query).map((queryEntry: any) => { Object.entries(payload).map((queryEntry: any) => {
const queryName: string = queryEntry[0] const queryName: string = queryEntry[0]
const queryOpts: PrometheusQueryOpts = queryEntry[1] const queryOpts: PrometheusQueryOpts = queryEntry[1]
const queries = prometheusProvider.getQueries(queryOpts) const queries = prometheusProvider.getQueries(queryOpts)
@ -78,7 +64,7 @@ class MetricsRoute extends LensApi {
return loadMetrics(q) return loadMetrics(q)
}) })
); );
Object.keys(query).forEach((metricName, index) => { Object.keys(payload).forEach((metricName, index) => {
data[metricName] = result[index]; data[metricName] = result[index];
}); });
} }

View File

@ -1,5 +1,5 @@
import { compile } from "path-to-regexp"; import { compile } from "path-to-regexp";
import { apiHelm } from "../index"; import { apiBase } from "../index";
import { stringify } from "querystring"; import { stringify } from "querystring";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
@ -21,7 +21,7 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
export const helmChartsApi = { export const helmChartsApi = {
list() { list() {
return apiHelm return apiBase
.get<IHelmChartList>(endpoint()) .get<IHelmChartList>(endpoint())
.then(data => { .then(data => {
return Object return Object
@ -33,7 +33,7 @@ export const helmChartsApi = {
get(repo: string, name: string, readmeVersion?: string) { get(repo: string, name: string, readmeVersion?: string) {
const path = endpoint({ repo, name }); const path = endpoint({ repo, name });
return apiHelm return apiBase
.get<IHelmChartDetails>(path + "?" + stringify({ version: readmeVersion })) .get<IHelmChartDetails>(path + "?" + stringify({ version: readmeVersion }))
.then(data => { .then(data => {
const versions = data.versions.map(HelmChart.create); const versions = data.versions.map(HelmChart.create);
@ -46,7 +46,7 @@ export const helmChartsApi = {
}, },
getValues(repo: string, name: string, version: string) { getValues(repo: string, name: string, version: string) {
return apiHelm return apiBase
.get<string>(`/v2/charts/${repo}/${name}/values?` + stringify({ version })); .get<string>(`/v2/charts/${repo}/${name}/values?` + stringify({ version }));
} }
}; };

View File

@ -2,7 +2,7 @@ import jsYaml from "js-yaml";
import { compile } from "path-to-regexp"; import { compile } from "path-to-regexp";
import { autobind, formatDuration } from "../../utils"; import { autobind, formatDuration } from "../../utils";
import capitalize from "lodash/capitalize"; import capitalize from "lodash/capitalize";
import { apiHelm } from "../index"; import { apiBase } from "../index";
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store"; import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
import { ItemObject } from "../../item.store"; import { ItemObject } from "../../item.store";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
@ -69,14 +69,14 @@ const endpoint = compile(`/v2/releases/:namespace?/:name?`) as (
export const helmReleasesApi = { export const helmReleasesApi = {
list(namespace?: string) { list(namespace?: string) {
return apiHelm return apiBase
.get<HelmRelease[]>(endpoint({ namespace })) .get<HelmRelease[]>(endpoint({ namespace }))
.then(releases => releases.map(HelmRelease.create)); .then(releases => releases.map(HelmRelease.create));
}, },
get(name: string, namespace: string) { get(name: string, namespace: string) {
const path = endpoint({ name, namespace }); const path = endpoint({ name, namespace });
return apiHelm.get<IReleaseRawDetails>(path).then(details => { return apiBase.get<IReleaseRawDetails>(path).then(details => {
const items: KubeObject[] = JSON.parse(details.resources).items; const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item)); const resources = items.map(item => KubeObject.create(item));
return { return {
@ -90,34 +90,34 @@ export const helmReleasesApi = {
const { repo, ...data } = payload; const { repo, ...data } = payload;
data.chart = `${repo}/${data.chart}`; data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values); data.values = jsYaml.safeLoad(data.values);
return apiHelm.post(endpoint(), { data }); return apiBase.post(endpoint(), { data });
}, },
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> { update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload; const { repo, ...data } = payload;
data.chart = `${repo}/${data.chart}`; data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values); data.values = jsYaml.safeLoad(data.values);
return apiHelm.put(endpoint({ name, namespace }), { data }); return apiBase.put(endpoint({ name, namespace }), { data });
}, },
async delete(name: string, namespace: string) { async delete(name: string, namespace: string) {
const path = endpoint({ name, namespace }); const path = endpoint({ name, namespace });
return apiHelm.del(path); return apiBase.del(path);
}, },
getValues(name: string, namespace: string) { getValues(name: string, namespace: string) {
const path = endpoint({ name, namespace }) + "/values"; const path = endpoint({ name, namespace }) + "/values";
return apiHelm.get<string>(path); return apiBase.get<string>(path);
}, },
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> { getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const path = endpoint({ name, namespace }) + "/history"; const path = endpoint({ name, namespace }) + "/history";
return apiHelm.get(path); return apiBase.get(path);
}, },
rollback(name: string, namespace: string, revision: number) { rollback(name: string, namespace: string, revision: number) {
const path = endpoint({ name, namespace }) + "/rollback"; const path = endpoint({ name, namespace }) + "/rollback";
return apiHelm.put(path, { return apiBase.put(path, {
data: { data: {
revision: revision revision: revision
} }

View File

@ -1,7 +1,7 @@
import jsYaml from "js-yaml" import jsYaml from "js-yaml"
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeJsonApiData } from "../kube-json-api"; import { KubeJsonApiData } from "../kube-json-api";
import { apiResourceApplier } from "../index"; import { apiBase } from "../index";
import { apiManager } from "../api-manager"; import { apiManager } from "../api-manager";
export const resourceApplierApi = { export const resourceApplierApi = {
@ -13,7 +13,7 @@ export const resourceApplierApi = {
if (typeof resource === "string") { if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource); resource = jsYaml.safeLoad(resource);
} }
return apiResourceApplier return apiBase
.post<KubeJsonApiData[]>("/stack", { data: resource }) .post<KubeJsonApiData[]>("/stack", { data: resource })
.then(data => { .then(data => {
const items = data.map(obj => { const items = data.map(obj => {

View File

@ -1,25 +1,15 @@
import { JsonApi, JsonApiErrorParsed } from "./json-api"; import { JsonApi, JsonApiErrorParsed } from "./json-api";
import { KubeJsonApi } from "./kube-json-api"; import { KubeJsonApi } from "./kube-json-api";
import { Notifications } from "../components/notifications"; import { Notifications } from "../components/notifications";
import { apiPrefix, isDevelopment } from "../../common/vars"; import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars";
//-- JSON HTTP APIS
export const apiBase = new JsonApi({ export const apiBase = new JsonApi({
debug: isDevelopment, debug: isDevelopment,
apiPrefix: apiPrefix.BASE, apiPrefix: apiPrefix,
}); });
export const apiKube = new KubeJsonApi({ export const apiKube = new KubeJsonApi({
debug: isDevelopment, debug: isDevelopment,
apiPrefix: apiPrefix.KUBE_BASE, apiPrefix: apiKubePrefix,
});
export const apiHelm = new KubeJsonApi({
debug: isDevelopment,
apiPrefix: apiPrefix.KUBE_HELM,
});
export const apiResourceApplier = new KubeJsonApi({
debug: isDevelopment,
apiPrefix: apiPrefix.KUBE_RESOURCE_APPLIER,
}); });
// Common handler for HTTP api errors // Common handler for HTTP api errors
@ -34,5 +24,3 @@ function onApiError(error: JsonApiErrorParsed, res: Response) {
apiBase.onError.addListener(onApiError); apiBase.onError.addListener(onApiError);
apiKube.onError.addListener(onApiError); apiKube.onError.addListener(onApiError);
apiHelm.onError.addListener(onApiError);
apiResourceApplier.onError.addListener(onApiError);

View File

@ -3,12 +3,19 @@
import merge from "lodash/merge" import merge from "lodash/merge"
import { stringify } from "querystring"; import { stringify } from "querystring";
import { IKubeObjectConstructor, KubeObject } from "./kube-object"; import { IKubeObjectConstructor, KubeObject } from "./kube-object";
import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api"; import { KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
import { apiKube } from "./index"; import { apiKube } from "./index";
import { kubeWatchApi } from "./kube-watch-api"; import { kubeWatchApi } from "./kube-watch-api";
import { apiManager } from "./api-manager"; import { apiManager } from "./api-manager";
import { createApiLink, parseApi } from "./kube-api-parse"; import { createApiLink, parseApi } from "./kube-api-parse";
export interface IKubeObjectRef {
kind: string;
apiVersion: string;
name: string;
namespace?: string;
}
export interface IKubeApiOptions<T extends KubeObject> { export interface IKubeApiOptions<T extends KubeObject> {
kind: string; // resource type within api-group, e.g. "Namespace" kind: string; // resource type within api-group, e.g. "Namespace"
apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods" apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods"

View File

@ -31,13 +31,6 @@ export interface KubeJsonApiData extends JsonApiData {
}; };
} }
export interface IKubeObjectRef {
kind: string;
apiVersion: string;
name: string;
namespace?: string;
}
export interface KubeJsonApiError extends JsonApiError { export interface KubeJsonApiError extends JsonApiError {
code: number; code: number;
status: string; status: string;
@ -49,14 +42,6 @@ export interface KubeJsonApiError extends JsonApiError {
}; };
} }
export interface IKubeJsonApiQuery {
watch?: any;
resourceVersion?: string;
timeoutSeconds?: number;
limit?: number; // doesn't work with ?watch
continue?: string; // might be used with ?limit from second request
}
export class KubeJsonApi extends JsonApi<KubeJsonApiData> { export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
protected parseError(error: KubeJsonApiError | any, res: Response): string[] { protected parseError(error: KubeJsonApiError | any, res: Response): string[] {
const { status, reason, message } = error; const { status, reason, message } = error;

View File

@ -29,7 +29,6 @@ export interface IKubeWatchRouteQuery {
export class KubeWatchApi { export class KubeWatchApi {
protected evtSource: EventSource; protected evtSource: EventSource;
protected onData = new EventEmitter<[IKubeWatchEvent]>(); protected onData = new EventEmitter<[IKubeWatchEvent]>();
protected apiUrl = apiPrefix.BASE + "/watch";
protected subscribers = observable.map<KubeApi, number>(); protected subscribers = observable.map<KubeApi, number>();
protected reconnectTimeoutMs = 5000; protected reconnectTimeoutMs = 5000;
protected maxReconnectsOnError = 10; protected maxReconnectsOnError = 10;
@ -79,7 +78,7 @@ export class KubeWatchApi {
return; return;
} }
const query = this.getQuery(); const query = this.getQuery();
const apiUrl = this.apiUrl + "?" + stringify(query); const apiUrl = `${apiPrefix}/watch?` + stringify(query);
this.evtSource = new EventSource(apiUrl); this.evtSource = new EventSource(apiUrl);
this.evtSource.onmessage = this.onMessage; this.evtSource.onmessage = this.onMessage;
this.evtSource.onerror = this.onError; this.evtSource.onerror = this.onError;