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

refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-11 11:32:29 +03:00
parent 8bb3954700
commit e72e28ddab
22 changed files with 133 additions and 218 deletions

View File

@ -13,11 +13,12 @@ export type ClusterId = string;
export interface ClusterModel { export interface ClusterModel {
id: ClusterId; id: ClusterId;
contextName: string;
kubeConfigPath: string;
kubeConfig?: string;
workspace?: string; workspace?: string;
preferences?: ClusterPreferences; preferences?: ClusterPreferences;
kubeConfigPath: string;
/** @deprecated */
kubeConfig?: string; // kube-config yaml
} }
export interface ClusterPreferences { export interface ClusterPreferences {

View File

@ -0,0 +1,5 @@
// Clone json-serializable object
export function cloneJsonObject<T = object>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}

View File

@ -4,3 +4,4 @@ export * from "./base64"
export * from "./camelCase" export * from "./camelCase"
export * from "./splitArray" export * from "./splitArray"
export * from "./randomFileName" export * from "./randomFileName"
export * from "./cloneJson"

View File

@ -2,11 +2,8 @@ import { app, remote } from "electron"
import { ensureDirSync, writeFileSync } from "fs-extra" import { ensureDirSync, writeFileSync } from "fs-extra"
import * as path from "path" import * as path from "path"
// todo: move to main/kubeconfig-manager.ts (?) // Write kubeconfigs to "embedded" store, i.e. "/Users/ixrock/Library/Application Support/Lens/kubeconfigs"
// Writes kubeconfigs to "embedded" store, i.e. .../Lens/kubeconfigs/
export function writeEmbeddedKubeConfig(clusterId: string, kubeConfig: string): string { export function writeEmbeddedKubeConfig(clusterId: string, kubeConfig: string): string {
// This can be called from main & renderer
const userData = (app || remote.app).getPath("userData"); const userData = (app || remote.app).getPath("userData");
const kubeConfigBase = path.join(userData, "kubeconfigs") const kubeConfigBase = path.join(userData, "kubeconfigs")
ensureDirSync(kubeConfigBase) ensureDirSync(kubeConfigBase)

View File

@ -89,6 +89,7 @@ export class ClusterManager {
} }
} }
// fixme
getClusterForRequest(req: http.IncomingMessage): Cluster { getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null let cluster: Cluster = null

View File

@ -1,6 +1,6 @@
import url, { UrlWithStringQuery } from "url"
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature" import type { FeatureStatusMap } from "./feature"
import { UrlWithStringQuery } from "url"
import { action, observable, toJS } from "mobx"; import { action, observable, toJS } from "mobx";
import { ContextHandler } from "./context-handler" import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
@ -31,16 +31,15 @@ export interface ClusterState extends ClusterModel {
} }
export class Cluster implements ClusterModel { export class Cluster implements ClusterModel {
public id: ClusterId;
public kubeCtl: Kubectl public kubeCtl: Kubectl
public contextHandler: ContextHandler; public contextHandler: ContextHandler;
protected kubeconfigManager: KubeconfigManager; protected kubeconfigManager: KubeconfigManager;
@observable initialized = false; @observable initialized = false;
@observable id: ClusterId;
@observable workspace: string; @observable workspace: string;
@observable kubeConfig?: string;
@observable kubeConfigPath: string;
@observable contextName: string; @observable contextName: string;
@observable kubeConfigPath: string;
@observable port: number; @observable port: number;
@observable url: string; // cluster-api url @observable url: string; // cluster-api url
@observable apiUrl: UrlWithStringQuery; // same as url, but parsed @observable apiUrl: UrlWithStringQuery; // same as url, but parsed
@ -63,20 +62,23 @@ export class Cluster implements ClusterModel {
updateModel(model: ClusterModel) { updateModel(model: ClusterModel) {
Object.assign(this, model); Object.assign(this, model);
if (!this.contextName) {
this.contextName = this.preferences.clusterName;
}
} }
@action @action
// fixme: completely broken
async init() { async init() {
try { try {
// fixme: all broken
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
this.contextName = this.contextHandler.contextName;
this.port = await this.contextHandler.resolveProxyPort(); // resolve port before KubeconfigManager this.port = await this.contextHandler.resolveProxyPort(); // resolve port before KubeconfigManager
this.webContentUrl = `http://${this.id}.localhost:${this.port}`; 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);
logger.info(`[CLUSTER]: init success`, { logger.info(`[CLUSTER]: init success`, {
id: this.id, id: this.id,
port: this.port, port: this.port,
@ -102,7 +104,7 @@ export class Cluster implements ClusterModel {
// todo: auto-refresh when preferences changed + by timer? // todo: auto-refresh when preferences changed + by timer?
@action @action
async refreshCluster() { async refreshCluster() {
this.contextHandler.setClusterPreferences(this.preferences); this.contextHandler.setupPrometheus(this.preferences);
const connectionStatus = await this.getConnectionStatus() const connectionStatus = await this.getConnectionStatus()
this.accessible = connectionStatus == ClusterStatus.AccessGranted; this.accessible = connectionStatus == ClusterStatus.AccessGranted;

View File

@ -9,29 +9,18 @@ import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy" import { KubeAuthProxy } from "./kube-auth-proxy"
export class ContextHandler { export class ContextHandler {
public url: string
public proxyPort: number public proxyPort: number
public contextName: string
protected id: string
protected proxyServer: KubeAuthProxy protected proxyServer: KubeAuthProxy
protected apiTarget: ServerOptions protected apiTarget: ServerOptions
protected certData: string
protected authCertData: string
protected proxyTarget: ServerOptions
protected clientCert: string
protected clientKey: string
protected prometheusProvider: string protected prometheusProvider: string
protected prometheusPath: string protected prometheusPath: string
constructor(protected cluster: Cluster) { constructor(protected cluster: Cluster) {
this.id = cluster.id this.setupPrometheus(cluster.preferences)
this.url = cluster.url;
this.contextName = cluster.contextName || cluster.preferences.clusterName;
this.setClusterPreferences(cluster.preferences)
} }
public setClusterPreferences(preferences: ClusterPreferences = {}) { public setupPrometheus(preferences: ClusterPreferences = {}) {
this.prometheusProvider = preferences.prometheusProvider?.type; this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheusPath = null; this.prometheusPath = null;
if (preferences.prometheus) { if (preferences.prometheus) {
@ -89,6 +78,7 @@ export class ContextHandler {
return apiTarget return apiTarget
} }
// fixme
protected async newApiTarget(timeout: number): Promise<ServerOptions> { protected async newApiTarget(timeout: number): Promise<ServerOptions> {
return { return {
changeOrigin: true, changeOrigin: true,
@ -107,27 +97,19 @@ export class ContextHandler {
async resolveProxyPort(): Promise<number> { async resolveProxyPort(): Promise<number> {
if (!this.proxyPort) { if (!this.proxyPort) {
this.proxyPort = await getFreePort() this.proxyPort = await getFreePort();
} }
return this.proxyPort return this.proxyPort
} }
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
try {
await callback(this.cluster.proxyKubeconfigPath())
} catch (error) {
throw(error)
}
}
public async ensureServer() { public async ensureServer() {
if (!this.proxyServer) { if (!this.proxyServer) {
const proxyPort = await this.resolveProxyPort() await this.resolveProxyPort();
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
} }
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv) this.proxyServer = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv)
await this.proxyServer.run() await this.proxyServer.run()
} }
} }
@ -139,7 +121,7 @@ export class ContextHandler {
} }
} }
public proxyServerError() { public proxyServerError(): string {
return this.proxyServer?.lastError || "" return this.proxyServer?.lastError || ""
} }
} }

View File

@ -2,7 +2,7 @@ import fs from "fs";
import path from "path" import path from "path"
import hb from "handlebars" import hb from "handlebars"
import { ResourceApplier } from "./resource-applier" import { ResourceApplier } from "./resource-applier"
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node" import { CoreV1Api, KubeConfig, Watch } from "@kubernetes/client-node"
import { Cluster } from "./cluster"; import { Cluster } from "./cluster";
import logger from "./logger"; import logger from "./logger";
@ -23,75 +23,61 @@ export interface FeatureStatus {
export abstract class Feature { export abstract class Feature {
name: string; name: string;
config: any;
latestVersion: string; latestVersion: string;
constructor(config: any) {
if(config) this.config = config;
}
// TODO Return types for these?
async install(cluster: Cluster): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
// Read and process yamls through handlebar
const resources = this.renderTemplates();
// Apply processed manifests
cluster.contextHandler.withTemporaryKubeconfig(async (kubeconfigPath) => {
const resourceApplier = new ResourceApplier(cluster, kubeconfigPath)
try {
await resourceApplier.kubectlApplyAll(resources)
resolve(true)
} catch(error) {
reject(error)
}
});
});
}
abstract async upgrade(cluster: Cluster): Promise<boolean>; abstract async upgrade(cluster: Cluster): Promise<boolean>;
abstract async uninstall(cluster: Cluster): Promise<boolean>; abstract async uninstall(cluster: Cluster): Promise<boolean>;
abstract async featureStatus(kc: KubeConfig): Promise<FeatureStatus>; abstract async featureStatus(kc: KubeConfig): Promise<FeatureStatus>;
constructor(public config: any) {
}
async install(cluster: Cluster): Promise<boolean> {
const resources = this.renderTemplates();
try {
await new ResourceApplier(cluster).kubectlApplyAll(resources);
return true;
} catch (err) {
logger.error("Installing feature error", { err, cluster });
return false
}
}
protected async deleteNamespace(kc: KubeConfig, name: string) { protected async deleteNamespace(kc: KubeConfig, name: string) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const client = kc.makeApiClient(CoreV1Api) const client = kc.makeApiClient(CoreV1Api)
const result = await client.deleteNamespace("lens-metrics", 'false', undefined, undefined, undefined, "Foreground"); const result = await client.deleteNamespace("lens-metrics", 'false', undefined, undefined, undefined, "Foreground");
const nsVersion = result.body.metadata.resourceVersion; const nsVersion = result.body.metadata.resourceVersion;
const nsWatch = new Watch(kc); const nsWatch = new Watch(kc);
const req = await nsWatch.watch('/api/v1/namespaces', {resourceVersion: nsVersion, fieldSelector: "metadata.name=lens-metrics"}, const query: Record<string, string> = {
(type, obj) => { resourceVersion: nsVersion,
if(type === 'DELETED') { fieldSelector: "metadata.name=lens-metrics",
}
const req = await nsWatch.watch('/api/v1/namespaces', query,
(phase, obj) => {
if (phase === 'DELETED') {
logger.debug(`namespace ${name} finally gone`) logger.debug(`namespace ${name} finally gone`)
req.abort(); req.abort();
resolve() resolve()
} }
}, },
(err) => { (err?: any) => {
if(err) { if (err) reject(err);
reject(err)
}
}); });
}); });
} }
protected renderTemplates(): string[] { protected renderTemplates(): string[] {
console.log("starting to render resources...");
const resources: string[] = []; const resources: string[] = [];
fs.readdirSync(this.manifestPath()).forEach((f) => { fs.readdirSync(this.manifestPath()).forEach(filename => {
const file = path.join(this.manifestPath(), f); const file = path.join(this.manifestPath(), filename);
console.log("processing file:", file)
const raw = fs.readFileSync(file); const raw = fs.readFileSync(file);
console.log("raw file loaded"); if (filename.endsWith('.hb')) {
if(f.endsWith('.hb')) {
console.log("processing HB template");
const template = hb.compile(raw.toString()); const template = hb.compile(raw.toString());
resources.push(template(this.config)); resources.push(template(this.config));
console.log("HB template done");
} else { } else {
console.log("using as raw, no HB detected");
resources.push(raw.toString()); resources.push(raw.toString());
} }
}); });
@ -101,7 +87,7 @@ export abstract class Feature {
protected manifestPath() { protected manifestPath() {
const devPath = path.join(__dirname, "..", 'src/features', this.name); const devPath = path.join(__dirname, "..", 'src/features', this.name);
if(fs.existsSync(devPath)) { if (fs.existsSync(devPath)) {
return devPath; return devPath;
} }
return path.join(__dirname, "..", 'features', this.name); return path.join(__dirname, "..", 'features', this.name);

View File

@ -99,7 +99,7 @@ export class HelmReleaseManager {
protected async getResources(name: string, namespace: string, cluster: Cluster) { protected async getResources(name: string, namespace: string, cluster: Cluster) {
const helm = await helmCli.binaryPath() const helm = await helmCli.binaryPath()
const kubectl = await cluster.kubeCtl.kubectlPath() const kubectl = await cluster.kubeCtl.getPath()
const pathToKubeconfig = cluster.proxyKubeconfigPath() const pathToKubeconfig = cluster.proxyKubeconfigPath()
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => { const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => {
return { stdout: JSON.stringify({items: []})} return { stdout: JSON.stringify({items: []})}

View File

@ -11,7 +11,7 @@ function resolveTilde(filePath: string) {
return filePath; return filePath;
} }
export function loadKubeConfig(pathOrContent?: string): KubeConfig { export function loadConfig(pathOrContent?: string): KubeConfig {
const kc = new KubeConfig(); const kc = new KubeConfig();
if (path.isAbsolute(pathOrContent)) { if (path.isAbsolute(pathOrContent)) {
kc.loadFromFile(resolveTilde(pathOrContent)); kc.loadFromFile(resolveTilde(pathOrContent));
@ -30,7 +30,7 @@ export function loadKubeConfig(pathOrContent?: string): KubeConfig {
*/ */
export function validateConfig(config: KubeConfig | string): KubeConfig { export function validateConfig(config: KubeConfig | string): KubeConfig {
if (typeof config == "string") { if (typeof config == "string") {
config = loadKubeConfig(config); config = loadConfig(config);
} }
logger.debug(`validating kube config: ${JSON.stringify(config)}`) logger.debug(`validating kube config: ${JSON.stringify(config)}`)
if (!config.users || config.users.length == 0) { if (!config.users || config.users.length == 0) {
@ -66,17 +66,6 @@ export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
return configs; return configs;
} }
/**
* Loads KubeConfig from a yaml and breaks it into several configs. Each context per KubeConfig object
*
* @param configPath path to kube config yaml file
*/
export function loadAndSplitConfig(configPath: string): KubeConfig[] {
const allConfigs = new KubeConfig();
allConfigs.loadFromFile(configPath);
return splitConfig(allConfigs);
}
export function dumpConfigYaml(kc: KubeConfig): string { export function dumpConfigYaml(kc: KubeConfig): string {
const config = { const config = {
apiVersion: "v1", apiVersion: "v1",
@ -122,8 +111,7 @@ export function dumpConfigYaml(kc: KubeConfig): string {
}) })
} }
console.log("dumping kc:", config); // logger.info("Dumping KubeConfig:", config);
// skipInvalid: true makes dump ignore undefined values // skipInvalid: true makes dump ignore undefined values
return yaml.safeDump(config, { skipInvalid: true }); return yaml.safeDump(config, { skipInvalid: true });
} }

View File

@ -25,7 +25,7 @@ export class KubeAuthProxy {
if (this.proxyProcess) { if (this.proxyProcess) {
return; return;
} }
const proxyBin = await this.kubectl.kubectlPath() const proxyBin = await this.kubectl.getPath()
let args = [ let args = [
"proxy", "proxy",
"-p", this.port.toString(), "-p", this.port.toString(),

View File

@ -1,13 +1,11 @@
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import { app } from "electron" import { app } from "electron"
import fs from "fs-extra" import fs from "fs-extra"
import { KubeConfig } from "@kubernetes/client-node"
import { randomFileName } from "../common/utils" import { randomFileName } from "../common/utils"
import { dumpConfigYaml, loadKubeConfig } from "./k8s" import { dumpConfigYaml, loadConfig } from "./k8s"
import logger from "./logger" import logger from "./logger"
export class KubeconfigManager { export class KubeconfigManager {
public config: KubeConfig;
protected configDir = app.getPath("temp") protected configDir = app.getPath("temp")
protected tempFile: string protected tempFile: string
@ -19,16 +17,6 @@ export class KubeconfigManager {
return this.tempFile; return this.tempFile;
} }
getCurrentClusterServer() {
return this.config.getCurrentCluster().server;
}
protected loadConfig() {
const { kubeConfigPath, kubeConfig } = this.cluster;
this.config = loadKubeConfig(kubeConfigPath || kubeConfig);
return this.config;
}
/** /**
* 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.
@ -36,12 +24,12 @@ export class KubeconfigManager {
protected createTemporaryKubeconfig(): string { protected createTemporaryKubeconfig(): string {
fs.ensureDir(this.configDir); fs.ensureDir(this.configDir);
const path = `${this.configDir}/${randomFileName("kubeconfig")}`; const path = `${this.configDir}/${randomFileName("kubeconfig")}`;
const { contextName, kubeAuthProxyUrl } = this.cluster; const { contextName, contextHandler, kubeConfigPath } = this.cluster;
const kubeConfig = this.loadConfig(); const kubeConfig = loadConfig(kubeConfigPath);
kubeConfig.clusters = [ kubeConfig.clusters = [
{ {
name: contextName, name: contextName,
server: kubeAuthProxyUrl, server: `http://127.0.0.1:${contextHandler.proxyPort}`, // fixme: extract
skipTLSVerify: true, skipTLSVerify: true,
} }
]; ];
@ -54,10 +42,11 @@ export class KubeconfigManager {
user: "proxy", user: "proxy",
name: contextName, name: contextName,
cluster: contextName, cluster: contextName,
// namespace: kubeConfig.getContextObject(contextName).namespace,
} }
]; ];
logger.info(`Creating temp config for context "${contextName}" at "${path}"`);
fs.writeFileSync(path, dumpConfigYaml(kubeConfig)); fs.writeFileSync(path, dumpConfigYaml(kubeConfig));
logger.info(`Created temp kube-config file for context "${this.cluster.contextName}" at "${path}"`);
return path; return path;
} }

View File

@ -98,7 +98,7 @@ export class Kubectl {
this.path = path.join(this.dirname, binaryName) this.path = path.join(this.dirname, binaryName)
} }
public async kubectlPath(): Promise<string> { public async getPath(): Promise<string> {
try { try {
await this.ensureKubectl() await this.ensureKubectl()
return this.path return this.path

View File

@ -4,7 +4,7 @@ import { Socket } from "net";
import * as url from "url"; import * as url from "url";
import * as WebSocket from "ws" import * as WebSocket from "ws"
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import * as shell from "./node-shell-session" import * as nodeShell from "./node-shell-session"
import { ClusterManager } from "./cluster-manager" import { ClusterManager } from "./cluster-manager"
import { Router } from "./router" import { Router } from "./router"
import { apiPrefix } from "../common/vars"; import { apiPrefix } from "../common/vars";
@ -84,12 +84,13 @@ export class LensProxy {
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
const timeoutMs = retryCount * 250
if (retryCount < 20) { if (retryCount < 20) {
logger.debug("Retrying proxy request to url: " + retryCounterKey) logger.debug("Retrying proxy request to url: " + retryCounterKey)
setTimeout(() => { setTimeout(() => {
this.retryCounters.set(retryCounterKey, retryCount + 1) this.retryCounters.set(retryCounterKey, retryCount + 1)
this.handleRequest(proxy, req, res) this.handleRequest(proxy, req, res)
}, (250 * retryCount)) }, timeoutMs)
} }
} }
} }
@ -102,23 +103,13 @@ export class LensProxy {
return proxy; return proxy;
} }
protected createWsListener() { protected createWsListener(): WebSocket.Server {
const ws = new WebSocket.Server({ noServer: true }) const ws = new WebSocket.Server({ noServer: true })
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => { return ws.on("connection", (async (socket: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req) const cluster = this.clusterManager.getClusterForRequest(req)
const contextHandler = cluster.contextHandler
const nodeParam = url.parse(req.url, true).query["node"]?.toString(); const nodeParam = url.parse(req.url, true).query["node"]?.toString();
await nodeShell.open(socket, cluster, nodeParam);
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => { }));
return new Promise<boolean>(async (resolve, reject) => {
const shellSession = await shell.open(con, kubeconfigPath, cluster, nodeParam)
shellSession.on("exit", () => {
resolve(true)
})
})
})
}))
return ws
} }
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> { protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
@ -146,7 +137,7 @@ 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 route when isBoom==true? this.router.route(cluster, req, res); // todo: handle "not-found" if isBoom==true?
} }
} }

View File

@ -13,15 +13,15 @@ export class NodeShellSession extends ShellSession {
protected podId: string protected podId: string
protected kc: KubeConfig protected kc: KubeConfig
constructor(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster, nodeName: string) { constructor(socket: WebSocket, cluster: Cluster, nodeName: string) {
super(socket, pathToKubeconfig, cluster) super(socket, cluster)
this.nodeName = nodeName this.nodeName = nodeName
this.podId = `node-shell-${uuid()}` this.podId = `node-shell-${uuid()}`
this.kc = cluster.proxyKubeconfig() this.kc = cluster.proxyKubeconfig()
} }
public async open() { public async open() {
const shell = await this.kubectl.kubectlPath() const shell = await this.kubectl.getPath()
let args = [] let args = []
if (this.createNodeShellPod(this.podId, this.nodeName)) { if (this.createNodeShellPod(this.podId, this.nodeName)) {
await this.waitForRunningPod(this.podId).catch((error) => { await this.waitForRunningPod(this.podId).catch((error) => {
@ -133,15 +133,9 @@ export class NodeShellSession extends ShellSession {
} }
} }
export async function open(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster, nodeName?: string): Promise<ShellSession> { export async function open(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise<ShellSession> {
return new Promise(async (resolve, reject) => { if (nodeName) {
let shell = null return new NodeShellSession(socket, cluster, nodeName)
if (nodeName) { }
shell = new NodeShellSession(socket, pathToKubeconfig, cluster, nodeName) return new ShellSession(socket, cluster);
} else {
shell = new ShellSession(socket, pathToKubeconfig, cluster)
}
shell.open()
resolve(shell)
})
} }

View File

@ -1,6 +1,8 @@
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 ?
const getNextAvailablePort = () => { const getNextAvailablePort = () => {
logger.debug("getNextAvailablePort() start") logger.debug("getNextAvailablePort() start")
const server = createServer() const server = createServer()

View File

@ -1,14 +1,14 @@
import { LensApiRequest } from "./router" import { LensApiRequest } from "./router"
import * as resourceApplier from "./resource-applier" import { ResourceApplier } from "./resource-applier"
import { LensApi } from "./lens-api" import { LensApi } from "./lens-api"
class ResourceApplierApi extends LensApi { class ResourceApplierApi extends LensApi {
public async applyResource(request: LensApiRequest) { public async applyResource(request: LensApiRequest) {
const { response, cluster, payload } = request const { response, cluster, payload } = request
try { try {
const resource = await resourceApplier.apply(cluster, cluster.proxyKubeconfigPath(), payload) const resource = await new ResourceApplier(cluster).apply(payload);
this.respondJson(response, [resource], 200) this.respondJson(response, [resource], 200)
} catch(error) { } catch (error) {
this.respondText(response, error, 422) this.respondText(response, error, 422)
} }
} }

View File

@ -1,47 +1,31 @@
import type { Cluster } from "./cluster";
import { KubernetesObject } from "@kubernetes/client-node"
import { exec } from "child_process"; import { exec } from "child_process";
import fs from "fs"; import fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import path from "path"; import path from "path";
import * as tempy from "tempy"; import * as tempy from "tempy";
import logger from "./logger" import logger from "./logger"
import { Cluster } from "./cluster";
import { tracker } from "../common/tracker"; import { tracker } from "../common/tracker";
import { cloneJsonObject } from "../common/utils";
type KubeObject = {
status: {};
metadata?: {
resourceVersion: number;
annotations?: {
"kubectl.kubernetes.io/last-applied-configuration": string;
};
};
}
export class ResourceApplier { export class ResourceApplier {
protected kubeconfigPath: string; constructor(protected cluster: Cluster) {
protected cluster: Cluster
constructor(cluster: Cluster, pathToKubeconfig: string) {
this.kubeconfigPath = pathToKubeconfig
this.cluster = cluster
} }
public async apply(resource: any): Promise<string> { async apply(resource: KubernetesObject | any): Promise<string> {
this.sanitizeObject((resource as KubeObject)) resource = this.sanitizeObject(resource);
try { tracker.event("resource", "apply")
tracker.event("resource", "apply") return await this.kubectlApply(yaml.safeDump(resource));
return await this.kubectlApply(yaml.safeDump(resource))
} catch(error) {
throw (error)
}
} }
protected async kubectlApply(content: string): Promise<string> { protected async kubectlApply(content: string): Promise<string> {
const kubectl = await this.cluster.kubeCtl.kubectlPath() const { kubeCtl, kubeConfigPath } = this.cluster;
const kubectlPath = await kubeCtl.getPath()
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
const fileName = tempy.file({name: "resource.yaml"}) const fileName = tempy.file({ name: "resource.yaml" })
fs.writeFileSync(fileName, content) fs.writeFileSync(fileName, content)
const cmd = `"${kubectl}" apply --kubeconfig ${this.kubeconfigPath} -o json -f ${fileName}` const cmd = `"${kubectlPath}" apply --kubeconfig ${kubeConfigPath} -o json -f ${fileName}`
logger.debug("shooting manifests with: " + cmd); logger.debug("shooting manifests with: " + cmd);
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env) const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env)
const httpsProxy = this.cluster.preferences?.httpsProxy const httpsProxy = this.cluster.preferences?.httpsProxy
@ -62,17 +46,18 @@ export class ResourceApplier {
} }
public async kubectlApplyAll(resources: string[]): Promise<string> { public async kubectlApplyAll(resources: string[]): Promise<string> {
const kubectl = await this.cluster.kubeCtl.kubectlPath() const { kubeCtl, kubeConfigPath } = this.cluster;
return new Promise<string>((resolve, reject) => { const kubectlPath = await kubeCtl.getPath()
return new Promise((resolve, reject) => {
const tmpDir = tempy.directory() const tmpDir = tempy.directory()
// Dump each resource into tmpDir // Dump each resource into tmpDir
for (const i in resources) { resources.forEach((resource, index) => {
fs.writeFileSync(path.join(tmpDir, `${i}.yaml`), resources[i]) fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
} })
const cmd = `"${kubectl}" apply --kubeconfig ${this.kubeconfigPath} -o json -f ${tmpDir}` const cmd = `"${kubectlPath}" apply --kubeconfig ${kubeConfigPath} -o json -f ${tmpDir}`
console.log("shooting manifests with:", cmd); console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout, stderr) => { exec(cmd, (error, stdout, stderr) => {
if(error) { if (error) {
reject("Error applying manifests:" + error); reject("Error applying manifests:" + error);
} }
if (stderr != "") { if (stderr != "") {
@ -84,18 +69,14 @@ export class ResourceApplier {
}) })
} }
protected sanitizeObject(resource: KubeObject) { protected sanitizeObject(resource: KubernetesObject | any) {
delete resource['status'] resource = cloneJsonObject(resource);
if (resource['metadata']) { delete resource.status;
if (resource['metadata']['annotations'] && resource['metadata']['annotations']['kubectl.kubernetes.io/last-applied-configuration']) { delete resource.metadata?.resourceVersion;
delete resource['metadata']['annotations']['kubectl.kubernetes.io/last-applied-configuration'] const annotations = resource.metadata?.annotations;
} if (annotations) {
delete resource['metadata']['resourceVersion'] delete annotations['kubectl.kubernetes.io/last-applied-configuration'];
} }
return resource;
} }
} }
export async function apply(cluster: Cluster, pathToKubeconfig: string, resource: any) {
const resourceApplier = new ResourceApplier(cluster, pathToKubeconfig)
return await resourceApplier.apply(resource)
}

View File

@ -2,6 +2,7 @@ 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;
@ -11,6 +12,7 @@ class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest) { public async routeMetrics(request: LensApiRequest) {
const { response, cluster } = request const { response, cluster } = request
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}` // fixme: extract
const query: IMetricsQuery = request.payload; const query: IMetricsQuery = request.payload;
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Host": `${cluster.id}.localhost:${cluster.port}`, "Host": `${cluster.id}.localhost:${cluster.port}`,
@ -25,7 +27,7 @@ class MetricsRoute extends LensApi {
let prometheusProvider: PrometheusProvider let prometheusProvider: PrometheusProvider
try { try {
const prometheusPath = await cluster.contextHandler.getPrometheusPath() const prometheusPath = await cluster.contextHandler.getPrometheusPath()
metricsUrl = `${cluster.kubeAuthProxyUrl}/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 cluster.contextHandler.getPrometheusProvider()
} catch { } catch {
this.respondJson(response, {}) this.respondJson(response, {})

View File

@ -37,7 +37,7 @@ class PortForward {
public async start() { public async start() {
this.localPort = await getFreePort() this.localPort = await getFreePort()
const kubectlBin = await bundledKubectl.kubectlPath() const kubectlBin = await bundledKubectl.getPath()
const args = [ const args = [
"--kubeconfig", this.kubeConfig, "--kubeconfig", this.kubeConfig,
"port-forward", "port-forward",

View File

@ -25,10 +25,10 @@ export class ShellSession extends EventEmitter {
protected running = false; protected running = false;
protected clusterId: string; protected clusterId: string;
constructor(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster) { constructor(socket: WebSocket, cluster: Cluster) {
super() super()
this.websocket = socket this.websocket = socket
this.kubeconfigPath = pathToKubeconfig this.kubeconfigPath = cluster.kubeConfigPath
this.kubectl = new Kubectl(cluster.version) this.kubectl = new Kubectl(cluster.version)
this.preferences = cluster.preferences || {} this.preferences = cluster.preferences || {}
this.clusterId = cluster.id this.clusterId = cluster.id

View File

@ -4,40 +4,33 @@ import path from "path"
import { app, remote } from "electron" import { app, remote } from "electron"
import { migration } from "../migration-wrapper"; import { migration } from "../migration-wrapper";
import { ensureDirSync } from "fs-extra" import { ensureDirSync } from "fs-extra"
import { KubeConfig } from "@kubernetes/client-node";
import { writeEmbeddedKubeConfig } from "../../common/utils/kubeconfig" import { writeEmbeddedKubeConfig } from "../../common/utils/kubeconfig"
import { ClusterModel } from "../../common/cluster-store"; import { ClusterModel } from "../../common/cluster-store";
export default migration({ export default migration({
version: "3.6.0-beta.1", version: "3.6.0-beta.1",
run(store, log: (...args: any[]) => void) { run(store, log: (...args: any[]) => void) {
const migratingClusters: ClusterModel[] = [] const migratedClusters: ClusterModel[] = []
const storedClusters: ClusterModel[] = store.get("clusters");
const kubeConfigBase = path.join((app || remote.app).getPath("userData"), "kubeconfigs") const kubeConfigBase = path.join((app || remote.app).getPath("userData"), "kubeconfigs")
ensureDirSync(kubeConfigBase)
const storedClusters: ClusterModel[] = store.get("clusters") if (!storedClusters) return;
if (!storedClusters) return ensureDirSync(kubeConfigBase);
log("Number of clusters to migrate: ", storedClusters.length) log("Number of clusters to migrate: ", storedClusters.length)
for (const cluster of storedClusters) { for (const cluster of storedClusters) {
try { try {
// take the embedded kubeconfig and dump it into a file // take the embedded kubeconfig and dump it into a file
cluster.kubeConfigPath = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig) cluster.kubeConfigPath = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig)
migratedClusters.push(cluster)
const kc = new KubeConfig()
kc.loadFromFile(cluster.kubeConfigPath)
cluster.contextName = kc.getCurrentContext()
delete cluster.kubeConfig
migratingClusters.push(cluster)
} catch (error) { } catch (error) {
log(`Failed to migrate Kubeconfig for cluster "${cluster.id}"`, error) log(`Failed to migrate Kubeconfig for cluster "${cluster.id}"`, error)
} }
} }
// "overwrite" the cluster configs // "overwrite" the cluster configs
if (migratingClusters.length > 0) { if (migratedClusters.length > 0) {
store.set("clusters", migratingClusters) store.set("clusters", migratedClusters)
} }
} }
}) })