From c46a5f6f207f93c060262ee434706645a2e8395e Mon Sep 17 00:00:00 2001 From: Jussi Nummelin Date: Thu, 11 Jun 2020 15:14:57 +0300 Subject: [PATCH] Use kubeconfigs as file references without copying them into store Signed-off-by: Jussi Nummelin --- src/common/cluster-store.ts | 10 +++- src/main/cluster-manager.ts | 48 ++++++++-------- src/main/cluster.ts | 29 +++++----- src/main/context-handler.ts | 55 ++++++++++--------- src/main/helm-release-manager.ts | 6 +- src/main/helm-service.ts | 12 ++-- src/main/k8s.ts | 8 +-- src/main/kube-auth-proxy.ts | 21 ++----- src/main/kubeconfig-manager.ts | 36 ++++++++++-- src/main/resource-applier-api.ts | 2 +- src/main/routes/port-forward.ts | 2 +- src/migrations/cluster-store/3.5.0-beta.1.ts | 32 +++++++++++ .../_vue/components/AddClusterPage.vue | 8 ++- 13 files changed, 168 insertions(+), 101 deletions(-) create mode 100644 src/migrations/cluster-store/3.5.0-beta.1.ts diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index af2eb826d9..ffbcff47f8 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -6,6 +6,7 @@ import * as version260Beta2 from "../migrations/cluster-store/2.6.0-beta.2" import * as version260Beta3 from "../migrations/cluster-store/2.6.0-beta.3" import * as version270Beta0 from "../migrations/cluster-store/2.7.0-beta.0" import * as version270Beta1 from "../migrations/cluster-store/2.7.0-beta.1" +import * as version350Beta1 from "../migrations/cluster-store/3.5.0-beta.1" import { getAppVersion } from "./utils/app-version"; export class ClusterStore { @@ -25,7 +26,8 @@ export class ClusterStore { "2.6.0-beta.2": version260Beta2.migration, "2.6.0-beta.3": version260Beta3.migration, "2.7.0-beta.0": version270Beta0.migration, - "2.7.0-beta.1": version270Beta1.migration + "2.7.0-beta.1": version270Beta1.migration, + "3.5.0-beta.1": version350Beta1.migration } }) } @@ -72,7 +74,8 @@ export class ClusterStore { const index = clusters.findIndex((cl) => cl.id === cluster.id) const storable = { id: cluster.id, - kubeConfig: cluster.kubeConfig, + kubeConfigPath: cluster.kubeConfigPath, + contextName: cluster.contextName, preferences: cluster.preferences, workspace: cluster.workspace } @@ -95,7 +98,8 @@ export class ClusterStore { public reloadCluster(cluster: ClusterBaseInfo): void { const storedCluster = this.getCluster(cluster.id); if (storedCluster) { - cluster.kubeConfig = storedCluster.kubeConfig + cluster.kubeConfigPath = storedCluster.kubeConfigPath + cluster.contextName = storedCluster.contextName cluster.preferences = storedCluster.preferences cluster.workspace = storedCluster.workspace } diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 53a68af8cf..3ebe861b20 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -44,12 +44,14 @@ export class ClusterManager { this.clusters = new Map() clusters.forEach((clusterInfo) => { try { - const kc = this.loadKubeConfig(clusterInfo.kubeConfig) + const kc = this.loadKubeConfig(clusterInfo.kubeConfigPath) + kc.setCurrentContext(clusterInfo.contextName) logger.debug(`Starting to load target definitions for ${ kc.currentContext }`) const cluster = new Cluster({ id: clusterInfo.id, port: this.port, - kubeConfig: clusterInfo.kubeConfig, + kubeConfigPath: clusterInfo.kubeConfigPath, + contextName: clusterInfo.contextName, preferences: clusterInfo.preferences, workspace: clusterInfo.workspace }) @@ -77,33 +79,35 @@ export class ClusterManager { clusters.map(cluster => cluster.stopServer()) } - protected loadKubeConfig(config: string): KubeConfig { + protected loadKubeConfig(configPath: string): KubeConfig { const kc = new KubeConfig(); - kc.loadFromString(config); + kc.loadFromFile(configPath) return kc; } protected async addNewCluster(clusterData: ClusterBaseInfo): Promise { return new Promise(async (resolve, reject) => { + + logger.info(`*******addNewCluster: ${JSON.stringify(clusterData)}`) + + try { - const configs: KubeConfig[] = k8s.loadAndSplitConfig(clusterData.kubeConfig) - if(configs.length == 0) { - reject("No cluster contexts defined") - } - configs.forEach(c => { - k8s.validateConfig(c) - const cluster = new Cluster({ - id: uuid(), - port: this.port, - kubeConfig: k8s.dumpConfigYaml(c), - preferences: clusterData.preferences, - workspace: clusterData.workspace - }) - cluster.init(c) - cluster.save() - this.clusters.set(cluster.id, cluster) - resolve(cluster) - }); + const kc = this.loadKubeConfig(clusterData.kubeConfigPath) + k8s.validateConfig(kc) + kc.setCurrentContext(clusterData.contextName) + const cluster = new Cluster({ + id: uuid(), + port: this.port, + kubeConfigPath: clusterData.kubeConfigPath, + contextName: clusterData.contextName, + preferences: clusterData.preferences, + workspace: clusterData.workspace + }) + cluster.init(kc) + cluster.save() + this.clusters.set(cluster.id, cluster) + resolve(cluster) + } catch(error) { logger.error(error) reject(error) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index e9aee0c28e..621ce9c46c 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -6,9 +6,9 @@ import logger from "./logger" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import * as fm from "./feature-manager"; import { Kubectl } from "./kubectl"; +import { KubeconfigManager } from "./kubeconfig-manager" import { PromiseIpc } from "electron-promise-ipc" import request from "request-promise-native" -import { KubeconfigManager } from "./kubeconfig-manager" import { apiPrefix } from "../common/vars"; enum ClusterStatus { @@ -19,7 +19,8 @@ enum ClusterStatus { export interface ClusterBaseInfo { id: string; - kubeConfig: string; + kubeConfigPath: string; + contextName: string; preferences?: ClusterPreferences; port?: number; workspace?: string; @@ -73,7 +74,7 @@ export class Cluster implements ClusterInfo { public isAdmin: boolean; public features: FeatureStatusMap; public kubeCtl: Kubectl - public kubeConfig: string; + public kubeConfigPath: string; public eventCount: number; public preferences: ClusterPreferences; @@ -85,16 +86,16 @@ export class Cluster implements ClusterInfo { constructor(clusterInfo: ClusterBaseInfo) { if (clusterInfo) Object.assign(this, clusterInfo) if (!this.preferences) this.preferences = {} - this.kubeconfigManager = new KubeconfigManager(this.kubeConfig) + this.kubeconfigManager = new KubeconfigManager(this) } - public kubeconfigPath() { + public proxyKubeconfigPath() { return this.kubeconfigManager.getPath() } public async init(kc: KubeConfig) { this.contextHandler = new ContextHandler(kc, this) - this.contextName = kc.currentContext + //this.contextName = kc.currentContext this.url = this.contextHandler.url this.apiUrl = kc.getCurrentCluster().server } @@ -138,15 +139,13 @@ export class Cluster implements ClusterInfo { this.eventCount = await this.getEventCount(); } - public updateKubeconfig(kubeconfig: string) { - const storedCluster = clusterStore.getCluster(this.id) - if (!storedCluster) { - return - } + // public updateKubeconfig(kubeconfig: string) { + // const storedCluster = clusterStore.getCluster(this.id) + // if (!storedCluster) { return } - this.kubeConfig = kubeconfig - this.save() - } + // this.kubeConfig = kubeconfig + // this.save() + // } public getPrometheusApiPrefix() { if (!this.preferences.prometheus?.prefix) { @@ -175,7 +174,7 @@ export class Cluster implements ClusterInfo { isAdmin: this.isAdmin, features: this.features, kubeCtl: this.kubeCtl, - kubeConfig: this.kubeConfig, + kubeConfigPath: this.kubeConfigPath, preferences: this.preferences } } diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index a5c12e9ffc..4bee1e3214 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -7,7 +7,9 @@ import { getFreePort } from "./port" import { KubeAuthProxy } from "./kube-auth-proxy" import { Cluster, ClusterPreferences } from "./cluster" import { prometheusProviders } from "../common/prometheus-providers" -import { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry" +import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry" +import { PrometheusLens } from "./prometheus/lens" +import { KubeconfigManager } from "./kubeconfig-manager" export class ContextHandler { public contextName: string @@ -35,36 +37,37 @@ export class ContextHandler { constructor(kc: KubeConfig, cluster: Cluster) { this.id = cluster.id - this.kc = new KubeConfig() - this.kc.users = [ - { - name: kc.getCurrentUser().name, - token: this.id - } - ] - this.kc.contexts = [ - { - name: kc.currentContext, - cluster: kc.getCurrentCluster().name, - user: kc.getCurrentUser().name, - namespace: kc.getContextObject(kc.currentContext).namespace - } - ] - this.kc.setCurrentContext(kc.currentContext) + this.kc = kc + logger.info(`**** ctxHandler.kc: ${JSON.stringify(kc, null, 2)}`) + // this.kc.users = [ + // { + // name: kc.getCurrentUser().name, + // token: this.id + // } + // ] + // this.kc.contexts = [ + // { + // name: kc.currentContext, + // cluster: kc.getCurrentCluster().name, + // user: kc.getCurrentUser().name, + // namespace: kc.getContextObject(kc.currentContext).namespace + // } + // ] + this.kc.setCurrentContext(cluster.contextName) this.cluster = cluster this.clusterUrl = url.parse(kc.getCurrentCluster().server) - this.contextName = kc.currentContext; + this.contextName = cluster.contextName; this.defaultNamespace = kc.getContextObject(kc.currentContext).namespace this.url = `http://${this.id}.localhost:${cluster.port}/` this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}` - this.kc.clusters = [ - { - name: kc.getCurrentCluster().name, - server: this.kubernetesApi, - skipTLSVerify: true - } - ] + // this.kc.clusters = [ + // { + // name: kc.getCurrentCluster().name, + // server: this.kubernetesApi, + // skipTLSVerify: true + // } + // ] this.setClusterPreferences(cluster.preferences) } @@ -174,7 +177,7 @@ export class ContextHandler { public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise) { try { - await callback(this.cluster.kubeconfigPath()) + await callback(this.cluster.proxyKubeconfigPath()) } catch (error) { throw(error) } diff --git a/src/main/helm-release-manager.ts b/src/main/helm-release-manager.ts index 8d95d3912c..e1e9137f3f 100644 --- a/src/main/helm-release-manager.ts +++ b/src/main/helm-release-manager.ts @@ -54,7 +54,7 @@ export class HelmReleaseManager { await fs.promises.writeFile(fileName, yaml.safeDump(values)) try { - const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.kubeconfigPath()}`).catch((error) => { throw(error.stderr)}) + const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.proxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)}) return { log: stdout, release: this.getRelease(name, namespace, cluster) @@ -66,7 +66,7 @@ export class HelmReleaseManager { public async getRelease(name: string, namespace: string, cluster: Cluster) { const helm = await helmCli.binaryPath() - const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.kubeconfigPath()}`).catch((error) => { throw(error.stderr)}) + const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.proxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)}) const release = JSON.parse(stdout) release.resources = await this.getResources(name, namespace, cluster) return release @@ -100,7 +100,7 @@ export class HelmReleaseManager { protected async getResources(name: string, namespace: string, cluster: Cluster) { const helm = await helmCli.binaryPath() const kubectl = await cluster.kubeCtl.kubectlPath() - const pathToKubeconfig = cluster.kubeconfigPath() + 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) => { return { stdout: JSON.stringify({items: []})} }) diff --git a/src/main/helm-service.ts b/src/main/helm-service.ts index c4e60e2c74..ee76e814b8 100644 --- a/src/main/helm-service.ts +++ b/src/main/helm-service.ts @@ -6,7 +6,7 @@ import { releaseManager } from "./helm-release-manager"; class HelmService { public async installChart(cluster: Cluster, data: {chart: string; values: {}; name: string; namespace: string; version: string}) { - const installResult = await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.kubeconfigPath()) + const installResult = await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.proxyKubeconfigPath()) return installResult } @@ -48,7 +48,7 @@ class HelmService { public async listReleases(cluster: Cluster, namespace: string = null) { await repoManager.init() - const releases = await releaseManager.listReleases(cluster.kubeconfigPath(), namespace) + const releases = await releaseManager.listReleases(cluster.proxyKubeconfigPath(), namespace) return releases } @@ -60,19 +60,19 @@ class HelmService { public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) { logger.debug("Fetch release values") - const values = await releaseManager.getValues(releaseName, namespace, cluster.kubeconfigPath()) + const values = await releaseManager.getValues(releaseName, namespace, cluster.proxyKubeconfigPath()) return values } public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { logger.debug("Fetch release history") - const history = await releaseManager.getHistory(releaseName, namespace, cluster.kubeconfigPath()) + const history = await releaseManager.getHistory(releaseName, namespace, cluster.proxyKubeconfigPath()) return(history) } public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) { logger.debug("Delete release") - const release = await releaseManager.deleteRelease(releaseName, namespace, cluster.kubeconfigPath()) + const release = await releaseManager.deleteRelease(releaseName, namespace, cluster.proxyKubeconfigPath()) return release } @@ -84,7 +84,7 @@ class HelmService { public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) { logger.debug("Rollback release") - const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.kubeconfigPath()) + const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.proxyKubeconfigPath()) return({ message: output }) } diff --git a/src/main/k8s.ts b/src/main/k8s.ts index 5d96df07d0..c89e82fb4b 100644 --- a/src/main/k8s.ts +++ b/src/main/k8s.ts @@ -70,13 +70,13 @@ export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] { } /** - * Loads KubeConfig from a yaml string and breaks it into several configs. Each context + * Loads KubeConfig from a yaml and breaks it into several configs. Each context per KubeConfig object * - * @param configString yaml string of kube config + * @param configPath path to kube config yaml file */ -export function loadAndSplitConfig(configString: string): k8s.KubeConfig[] { +export function loadAndSplitConfig(configPath: string): k8s.KubeConfig[] { const allConfigs = new k8s.KubeConfig(); - allConfigs.loadFromString(configString); + allConfigs.loadFromFile(configPath); return splitConfig(allConfigs); } diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 53569ac002..109a5f85b1 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -31,26 +31,18 @@ export class KubeAuthProxy { return; } const proxyBin = await this.kubectl.kubectlPath() - const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => { - if (eventType === "change") { - const kc = readFileSync(this.cluster.kubeconfigPath()).toString() - if (kc.trim().length > 0) { // Prevent updating empty configs back to store - this.cluster.updateKubeconfig(kc) - } else { - logger.warn(`kubeconfig watch on ${this.cluster.kubeconfigPath()} resulted into empty config, ignoring...`) - } - } - }) - const clusterUrl = url.parse(this.cluster.apiUrl) let args = [ "proxy", - "--port", this.port.toString(), - "--kubeconfig", this.cluster.kubeconfigPath(), - "--accept-hosts", clusterUrl.hostname, + "-p", this.port.toString(), + "--kubeconfig", this.cluster.kubeConfigPath, + "--context", this.cluster.contextName, + "--accept-hosts", ".*", + "--reject-paths", "^[^/]", ] if (process.env.DEBUG_PROXY === "true") { args = args.concat(["-v", "9"]) } + logger.debug(`spawning kubectl proxy with args: ${args}`) this.proxyProcess = spawn(proxyBin, args, { env: this.env }) @@ -60,7 +52,6 @@ export class KubeAuthProxy { logger.debug("failed to send IPC log message: " + err.message) }) this.proxyProcess = null - configWatcher.close() }) this.proxyProcess.stdout.on('data', (data) => { let logItem = data.toString() diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index ead41d9ab0..52d67abd0d 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -2,14 +2,17 @@ import { app } from "electron" import fs from "fs" import { ensureDir, randomFileName} from "./file-helpers" import logger from "./logger" +import { Cluster } from "./cluster" +import * as k8s from "./k8s" +import { KubeConfig } from "@kubernetes/client-node" export class KubeconfigManager { protected configDir = app.getPath("temp") - protected kubeconfig: string protected tempFile: string + protected cluster: Cluster - constructor(kubeconfig: string) { - this.kubeconfig = kubeconfig + constructor(cluster: Cluster) { + this.cluster = cluster this.tempFile = this.createTemporaryKubeconfig() } @@ -21,7 +24,32 @@ export class KubeconfigManager { ensureDir(this.configDir) const path = `${this.configDir}/${randomFileName("kubeconfig")}` logger.debug('Creating temporary kubeconfig: ' + path) - fs.writeFileSync(path, this.kubeconfig) + logger.debug(`cluster url: ${this.cluster.url}`) + logger.debug(`cluster api url: ${this.cluster.apiUrl}`) + + // TODO Make the names come from actual cluster.name etc. + let kc = { + clusters: [ + { + name: this.cluster.contextName, + server: `http://127.0.0.1:${this.cluster.port}/${this.cluster.id}` + } + ], + users: [ + { + name: "proxy" + } + ], + contexts: [ + { + name: this.cluster.contextName, + cluster: this.cluster.contextName, + user: "proxy" + } + ], + currentContext: this.cluster.contextName + } as KubeConfig + fs.writeFileSync(path, k8s.dumpConfigYaml(kc)) return path } diff --git a/src/main/resource-applier-api.ts b/src/main/resource-applier-api.ts index 04df40a85f..04fa9f117a 100644 --- a/src/main/resource-applier-api.ts +++ b/src/main/resource-applier-api.ts @@ -6,7 +6,7 @@ class ResourceApplierApi extends LensApi { public async applyResource(request: LensApiRequest) { const { response, cluster, payload } = request try { - const resource = await resourceApplier.apply(cluster, cluster.kubeconfigPath(), payload) + const resource = await resourceApplier.apply(cluster, cluster.proxyKubeconfigPath(), payload) this.respondJson(response, [resource], 200) } catch(error) { this.respondText(response, error, 422) diff --git a/src/main/routes/port-forward.ts b/src/main/routes/port-forward.ts index 4b0a4077f0..27b1158700 100644 --- a/src/main/routes/port-forward.ts +++ b/src/main/routes/port-forward.ts @@ -87,7 +87,7 @@ class PortForwardRoute extends LensApi { namespace: params.namespace, name: params.service, port: params.port, - kubeConfig: cluster.kubeconfigPath() + kubeConfig: cluster.proxyKubeconfigPath() }) const started = await portForward.start() if (!started) { diff --git a/src/migrations/cluster-store/3.5.0-beta.1.ts b/src/migrations/cluster-store/3.5.0-beta.1.ts new file mode 100644 index 0000000000..e0c48b7fb4 --- /dev/null +++ b/src/migrations/cluster-store/3.5.0-beta.1.ts @@ -0,0 +1,32 @@ +// move embedded kubeconfig into separate file and add reference to it to cluster settings +import { app } from "electron" +import { ensureDirSync, writeFileSync } from "fs-extra" +import * as path from "path" + +export function migration(store: any) { + console.log("CLUSTER STORE, MIGRATION: 3.5.0-beta.1"); + const clusters: any[] = [] + + const kubeConfigBase = path.join(app.getPath("userData"), "kubeconfigs") + ensureDirSync(kubeConfigBase) + let storedClusters = store.get("clusters") as any[] + if (!storedClusters) return + + console.log("num clusters to migrate: ", storedClusters.length) + for (let cluster of storedClusters ) { + // take the embedded kubeconfig and dump it into a file + const kubeConfigFile = path.join(kubeConfigBase, cluster.id) + writeFileSync(kubeConfigFile, cluster.kubeConfig) + delete cluster.kubeConfig + cluster.kubeConfigPath = kubeConfigFile + // TODO Need to parse the context name from the config + + + // "overwrite" the cluster configs + clusters.push(cluster) + } + + if (clusters.length > 0) { + store.set("clusters", clusters) + } +} diff --git a/src/renderer/_vue/components/AddClusterPage.vue b/src/renderer/_vue/components/AddClusterPage.vue index 99aa013354..39a1c56030 100644 --- a/src/renderer/_vue/components/AddClusterPage.vue +++ b/src/renderer/_vue/components/AddClusterPage.vue @@ -113,6 +113,7 @@ import * as PrismEditor from 'vue-prism-editor' import * as k8s from "@kubernetes/client-node" import { dumpConfigYaml } from "../../../main/k8s" import ClustersMixin from "@/_vue/mixins/ClustersMixin"; +import * as path from "path" class ClusterAccessError extends Error {} @@ -196,8 +197,13 @@ export default { try { const kc = new k8s.KubeConfig(); kc.loadFromString(this.clusterconfig); // throws TypeError if we cannot parse kubeconfig + + // TODO Remove hardcoded path + const configPath = path.join(process.env.HOME, '.kube', 'config'); + const clusterInfo = { - kubeConfig: dumpConfigYaml(kc), + kubeConfigPath: configPath, + contextName: kc.currentContext, preferences: { clusterName: kc.currentContext },