From 374f066b862227ff2ddc33d0ce6c1998c37bf5ae Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 18 Nov 2020 17:28:45 +0200 Subject: [PATCH] split cluster to managed cluster Signed-off-by: Jari Kolehmainen --- src/common/cluster-ipc.ts | 25 +- src/extensions/cluster-feature.ts | 4 +- src/main/__test__/cluster.test.ts | 78 ----- src/main/__test__/kubeconfig-manager.test.ts | 5 +- src/main/__test__/managed-cluster.test.ts | 160 +++++++++ src/main/cluster-manager.ts | 46 ++- src/main/cluster.ts | 323 +----------------- src/main/context-handler.ts | 14 +- src/main/helm/helm-release-manager.ts | 12 +- src/main/helm/helm-service.ts | 18 +- src/main/lens-proxy.ts | 8 +- src/main/managed-cluster.ts | 330 +++++++++++++++++++ src/main/node-shell-session.ts | 6 +- src/main/resource-applier.ts | 14 +- src/main/router.ts | 8 +- src/main/routes/kubeconfig-route.ts | 14 +- src/main/routes/metrics-route.ts | 4 +- src/main/routes/port-forward-route.ts | 4 +- src/main/shell-session.ts | 12 +- 19 files changed, 608 insertions(+), 477 deletions(-) create mode 100644 src/main/__test__/managed-cluster.test.ts create mode 100644 src/main/managed-cluster.ts diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index 15f4fef584..4a84e0304b 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -3,6 +3,7 @@ import { ClusterId, clusterStore } from "./cluster-store"; import { appEventBus } from "./event-bus" import { ResourceApplier } from "../main/resource-applier"; import { ipcMain } from "electron"; +import { ClusterManager } from "../main/cluster-manager"; export const clusterActivateHandler = "cluster:activate" export const clusterSetFrameIdHandler = "cluster:set-frame-id" @@ -10,35 +11,35 @@ export const clusterRefreshHandler = "cluster:refresh" export const clusterDisconnectHandler = "cluster:disconnect" export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all" +function getById(clusterId: ClusterId) { + return ClusterManager.getInstance().getClusterById(clusterId) +} + if (ipcMain) { handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) { - return cluster.activate(force); - } + return getById(clusterId)?.activate(force); }) handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId?: number) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) { - if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates - return cluster.pushState(); + const managedCluster = getById(clusterId); + if (managedCluster) { + if (frameId) managedCluster.cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates + return managedCluster.cluster.pushState(); } }) handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) return cluster.refresh({ refreshMetadata: true }) + getById(clusterId)?.refresh({ refreshMetadata: true }); }) handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => { appEventBus.emit({name: "cluster", action: "stop"}); - return clusterStore.getById(clusterId)?.disconnect(); + return getById(clusterId)?.disconnect(); }) handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => { appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}) - const cluster = clusterStore.getById(clusterId); + const cluster = getById(clusterId); if (cluster) { const applier = new ResourceApplier(cluster) applier.kubectlApplyAll(resources) diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts index 90cbd3a0a9..33d3ccd278 100644 --- a/src/extensions/cluster-feature.ts +++ b/src/extensions/cluster-feature.ts @@ -8,6 +8,7 @@ import logger from "../main/logger"; import { app } from "electron" import { requestMain } from "../common/ipc"; import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc"; +import { ClusterManager } from "../main/cluster-manager"; export interface ClusterFeatureStatus { currentVersion: string; @@ -38,7 +39,8 @@ export abstract class ClusterFeature { protected async applyResources(cluster: Cluster, resources: string[]) { if (app) { - await new ResourceApplier(cluster).kubectlApplyAll(resources) + const managedCluster = ClusterManager.getInstance().getClusterById(cluster.id) + await new ResourceApplier(managedCluster).kubectlApplyAll(resources) } else { await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources) } diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 52e984b195..b5bf05afc6 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -33,10 +33,6 @@ import { Console } from "console"; import mockFs from "mock-fs"; import { workspaceStore } from "../../common/workspace-store"; import { Cluster } from "../cluster" -import { ContextHandler } from "../context-handler"; -import { getFreePort } from "../port"; -import { V1ResourceAttributes } from "@kubernetes/client-node"; -import { apiResources } from "../../common/rbac"; import request from "request-promise-native" import { Kubectl } from "../kubectl"; @@ -90,78 +86,4 @@ describe("create clusters", () => { }) expect(c.apiUrl).toBe("https://192.168.64.3:8443") }) - - it("init should not throw if everything is in order", async () => { - const c = new Cluster({ - id: "foo", - contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId - }) - await c.init(await getFreePort()) - expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), { - id: "foo", - apiUrl: "https://192.168.64.3:8443", - context: "minikube", - }) - }) - - it("activating cluster should try to connect to cluster and do a refresh", async () => { - const port = await getFreePort() - jest.spyOn(ContextHandler.prototype, "ensureServer"); - - const mockListNSs = jest.fn() - const mockKC = { - makeApiClient() { - return { - listNamespace: mockListNSs, - } - } - } - jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true)) - jest.spyOn(Cluster.prototype, "canI") - .mockImplementationOnce((attr: V1ResourceAttributes): Promise => { - expect(attr.namespace).toBe("default") - expect(attr.resource).toBe("pods") - expect(attr.verb).toBe("list") - return Promise.resolve(true) - }) - .mockImplementation((attr: V1ResourceAttributes): Promise => { - expect(attr.namespace).toBe("default") - expect(attr.verb).toBe("list") - return Promise.resolve(true) - }) - jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any) - mockListNSs.mockImplementationOnce(() => ({ - body: { - items: [{ - metadata: { - name: "default", - } - }] - } - })) - - mockedRequest.mockImplementationOnce(((uri: any, _options: any) => { - expect(uri).toBe(`http://localhost:${port}/api-kube/version`) - return Promise.resolve({ gitVersion: "1.2.3" }) - }) as any) - - const c = new Cluster({ - id: "foo", - contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId - }) - await c.init(port) - await c.activate() - - expect(ContextHandler.prototype.ensureServer).toBeCalled() - expect(mockedRequest).toBeCalled() - expect(c.accessible).toBe(true) - expect(c.allowedNamespaces.length).toBe(1) - expect(c.allowedResources.length).toBe(apiResources.length) - c.disconnect() - jest.resetAllMocks() - }) }) diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index a0a4060111..2e989caf28 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -26,6 +26,7 @@ jest.mock("winston", () => ({ import { KubeconfigManager } from "../kubeconfig-manager" import mockFs from "mock-fs" import { Cluster } from "../cluster"; +import { ManagedCluster } from "../managed-cluster"; import { workspaceStore } from "../../common/workspace-store"; import { ContextHandler } from "../context-handler"; import { getFreePort } from "../port"; @@ -78,7 +79,7 @@ describe("kubeconfig manager tests", () => { kubeConfigPath: "minikube-config.yml", workspace: workspaceStore.currentWorkspaceId }) - const contextHandler = new ContextHandler(cluster) + const contextHandler = new ContextHandler(new ManagedCluster(cluster)) const port = await getFreePort() const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port) @@ -98,7 +99,7 @@ describe("kubeconfig manager tests", () => { kubeConfigPath: "minikube-config.yml", workspace: workspaceStore.currentWorkspaceId }) - const contextHandler = new ContextHandler(cluster) + const contextHandler = new ContextHandler(new ManagedCluster(cluster)) const port = await getFreePort() const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port) diff --git a/src/main/__test__/managed-cluster.test.ts b/src/main/__test__/managed-cluster.test.ts new file mode 100644 index 0000000000..c2607e3df7 --- /dev/null +++ b/src/main/__test__/managed-cluster.test.ts @@ -0,0 +1,160 @@ +const logger = { + silly: jest.fn(), + debug: jest.fn(), + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + crit: jest.fn(), +}; + +jest.mock("winston", () => ({ + format: { + colorize: jest.fn(), + combine: jest.fn(), + simple: jest.fn(), + label: jest.fn(), + timestamp: jest.fn(), + printf: jest.fn() + }, + createLogger: jest.fn().mockReturnValue(logger), + transports: { + Console: jest.fn(), + File: jest.fn(), + } +})) + + +jest.mock("../../common/ipc") +jest.mock("../context-handler") +jest.mock("request") +jest.mock("request-promise-native") + +import { Console } from "console"; +import mockFs from "mock-fs"; +import { workspaceStore } from "../../common/workspace-store"; +import { Cluster } from "../cluster" +import { ManagedCluster } from "../managed-cluster" +import { ContextHandler } from "../context-handler"; +import { getFreePort } from "../port"; +import { V1ResourceAttributes } from "@kubernetes/client-node"; +import { apiResources } from "../../common/rbac"; +import request from "request-promise-native" +import { Kubectl } from "../kubectl"; + +const mockedRequest = request as jest.MockedFunction + +console = new Console(process.stdout, process.stderr) // fix mockFS + +describe("managed clusters", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + beforeEach(() => { + const mockOpts = { + "minikube-config.yml": JSON.stringify({ + apiVersion: "v1", + clusters: [{ + name: "minikube", + cluster: { + server: "https://192.168.64.3:8443", + }, + }], + contexts: [{ + context: { + cluster: "minikube", + user: "minikube", + }, + name: "minikube", + }], + users: [{ + name: "minikube", + }], + kind: "Config", + preferences: {}, + }) + } + mockFs(mockOpts) + jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true)) + }) + + afterEach(() => { + mockFs.restore() + }) + + it("init should not throw if everything is in order", async () => { + const c = new Cluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath: "minikube-config.yml", + workspace: workspaceStore.currentWorkspaceId + }) + const managed = new ManagedCluster(c) + await managed.init(await getFreePort()) + expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), { + id: "foo", + apiUrl: "https://192.168.64.3:8443", + context: "minikube", + }) + }) + + it("activating cluster should try to connect to cluster and do a refresh", async () => { + const port = await getFreePort() + jest.spyOn(ContextHandler.prototype, "ensureServer"); + + const mockListNSs = jest.fn() + const mockKC = { + makeApiClient() { + return { + listNamespace: mockListNSs, + } + } + } + jest.spyOn(ManagedCluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true)) + jest.spyOn(ManagedCluster.prototype, "canI") + .mockImplementationOnce((attr: V1ResourceAttributes): Promise => { + expect(attr.namespace).toBe("default") + expect(attr.resource).toBe("pods") + expect(attr.verb).toBe("list") + return Promise.resolve(true) + }) + .mockImplementation((attr: V1ResourceAttributes): Promise => { + expect(attr.namespace).toBe("default") + expect(attr.verb).toBe("list") + return Promise.resolve(true) + }) + jest.spyOn(ManagedCluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any) + mockListNSs.mockImplementationOnce(() => ({ + body: { + items: [{ + metadata: { + name: "default", + } + }] + } + })) + + mockedRequest.mockImplementationOnce(((uri: any, _options: any) => { + expect(uri).toBe(`http://localhost:${port}/api-kube/version`) + return Promise.resolve({ gitVersion: "1.2.3" }) + }) as any) + + const c = new Cluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath: "minikube-config.yml", + workspace: workspaceStore.currentWorkspaceId + }) + const managed = new ManagedCluster(c) + await managed.init(port) + await managed.activate() + + expect(ContextHandler.prototype.ensureServer).toBeCalled() + expect(mockedRequest).toBeCalled() + expect(c.accessible).toBe(true) + expect(c.allowedNamespaces.length).toBe(1) + expect(c.allowedResources.length).toBe(apiResources.length) + managed.disconnect() + jest.resetAllMocks() + }) +}) diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 1a479e724e..7d9cc6b6c7 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -2,13 +2,17 @@ import "../common/cluster-ipc"; import type http from "http" import { ipcMain } from "electron" import { autorun } from "mobx"; -import { clusterStore, getClusterIdFromHost } from "../common/cluster-store" +import { ClusterId, clusterStore, getClusterIdFromHost } from "../common/cluster-store" import { Cluster } from "./cluster" +import { ManagedCluster } from "./managed-cluster" import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; import { Singleton } from "../common/utils"; export class ClusterManager extends Singleton { + protected managedClusters: Map = new Map() + + constructor(public readonly port: number) { super() // auto-init clusters @@ -16,7 +20,9 @@ export class ClusterManager extends Singleton { clusterStore.enabledClustersList.forEach(cluster => { if (!cluster.initialized) { logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); - cluster.init(port); + const managedCluster = new ManagedCluster(cluster) + managedCluster.init(port) + this.managedClusters.set(cluster.id, managedCluster) } }); }); @@ -27,7 +33,12 @@ export class ClusterManager extends Singleton { if (removedClusters.length > 0) { const meta = removedClusters.map(cluster => cluster.getMeta()); logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); - removedClusters.forEach(cluster => cluster.disconnect()); + removedClusters.forEach((cluster) => { + const managedCluster = this.managedClusters.get(cluster.id) + if (managedCluster) { + managedCluster.disconnect() + } + }); clusterStore.removedClusters.clear(); } }, { @@ -44,7 +55,10 @@ export class ClusterManager extends Singleton { if (!cluster.disconnected) { cluster.online = false cluster.accessible = false - cluster.refreshConnectionStatus().catch((e) => e) + const managedCluster = this.managedClusters.get(cluster.id) + if (managedCluster) { + managedCluster.refreshConnectionStatus().catch((e) => e) + } } }) } @@ -53,33 +67,43 @@ export class ClusterManager extends Singleton { logger.info("[CLUSTER-MANAGER]: network is online") clusterStore.enabledClustersList.forEach((cluster) => { if (!cluster.disconnected) { - cluster.refreshConnectionStatus().catch((e) => e) + const managedCluster = this.managedClusters.get(cluster.id) + if (managedCluster) { + managedCluster.refreshConnectionStatus().catch((e) => e) + } } }) } stop() { clusterStore.clusters.forEach((cluster: Cluster) => { - cluster.disconnect(); + const managedCluster = this.managedClusters.get(cluster.id) + if (managedCluster) { + managedCluster.disconnect() + } }) } - getClusterForRequest(req: http.IncomingMessage): Cluster { - let cluster: Cluster = null + getClusterById(id: ClusterId) { + return this.managedClusters.get(id) + } + + getClusterForRequest(req: http.IncomingMessage): ManagedCluster { + let cluster: ManagedCluster = null // lens-server is connecting to 127.0.0.1:/ if (req.headers.host.startsWith("127.0.0.1")) { const clusterId = req.url.split("/")[1] - cluster = clusterStore.getById(clusterId) + cluster = this.managedClusters.get(clusterId) if (cluster) { // we need to swap path prefix so that request is proxied to kube api req.url = req.url.replace(`/${clusterId}`, apiKubePrefix) } } else if (req.headers["x-cluster-id"]) { - cluster = clusterStore.getById(req.headers["x-cluster-id"].toString()) + cluster = this.managedClusters.get(req.headers["x-cluster-id"].toString()) } else { const clusterId = getClusterIdFromHost(req.headers.host); - cluster = clusterStore.getById(clusterId) + cluster = this.managedClusters.get(clusterId) } return cluster; diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 28dce65f0b..beb12538cf 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,26 +1,11 @@ -import { ipcMain } from "electron" import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store" -import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; -import { action, computed, observable, reaction, toJS, when } from "mobx"; -import { apiKubePrefix } from "../common/vars"; +import { action, computed, observable, toJS, when } from "mobx"; import { broadcastMessage } from "../common/ipc"; -import { ContextHandler } from "./context-handler" -import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" +import { KubeConfig } from "@kubernetes/client-node" import { Kubectl } from "./kubectl"; -import { KubeconfigManager } from "./kubeconfig-manager" -import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers" -import request, { RequestPromiseOptions } from "request-promise-native" -import { apiResources } from "../common/rbac"; -import logger from "./logger" -import { VersionDetector } from "./cluster-detectors/version-detector"; -import { detectorRegistry } from "./cluster-detectors/detector-registry"; - -export enum ClusterStatus { - AccessGranted = 2, - AccessDenied = 1, - Offline = 0 -} +import { loadConfig } from "../common/kube-helpers" +import logger from "./logger"; export enum ClusterMetadataKey { VERSION = "version", @@ -30,9 +15,7 @@ export enum ClusterMetadataKey { LAST_SEEN = "lastSeen" } -export type ClusterRefreshOptions = { - refreshMetadata?: boolean -} + export interface ClusterState { initialized: boolean; @@ -52,11 +35,7 @@ export class Cluster implements ClusterModel, ClusterState { public id: ClusterId; public frameId: number; public kubeCtl: Kubectl - public contextHandler: ContextHandler; public ownerRef: string; - protected kubeconfigManager: KubeconfigManager; - protected eventDisposers: Function[] = []; - protected activated = false; whenInitialized = when(() => this.initialized); whenReady = when(() => this.ready); @@ -111,258 +90,10 @@ export class Cluster implements ClusterModel, ClusterState { Object.assign(this, model); } - @action - async init(port: number) { - try { - this.contextHandler = new ContextHandler(this); - this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port); - this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; - this.initialized = true; - logger.info(`[CLUSTER]: "${this.contextName}" init success`, { - id: this.id, - context: this.contextName, - apiUrl: this.apiUrl - }); - } catch (err) { - logger.error(`[CLUSTER]: init failed: ${err}`, { - id: this.id, - error: err, - }); - } - } - - protected bindEvents() { - logger.info(`[CLUSTER]: bind events`, this.getMeta()) - const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000) // every 30s - const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000) // every 15 minutes - - if (ipcMain) { - this.eventDisposers.push( - reaction(() => this.getState(), () => this.pushState()), - () => { - clearInterval(refreshTimer) - clearInterval(refreshMetadataTimer) - }, - ); - } - } - - protected unbindEvents() { - logger.info(`[CLUSTER]: unbind events`, this.getMeta()); - this.eventDisposers.forEach(dispose => dispose()); - this.eventDisposers.length = 0; - } - - @action - async activate(force = false) { - if (this.activated && !force) { - return this.pushState(); - } - logger.info(`[CLUSTER]: activate`, this.getMeta()); - await this.whenInitialized; - if (!this.eventDisposers.length) { - this.bindEvents(); - } - if (this.disconnected || !this.accessible) { - await this.reconnect(); - } - await this.refreshConnectionStatus() - if (this.accessible) { - await this.refreshAllowedResources() - this.isAdmin = await this.isClusterAdmin() - this.ready = true - this.kubeCtl = new Kubectl(this.version) - this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard - } - this.activated = true - return this.pushState(); - } - - @action - async reconnect() { - logger.info(`[CLUSTER]: reconnect`, this.getMeta()); - this.contextHandler.stopServer(); - await this.contextHandler.ensureServer(); - this.disconnected = false; - } - - @action - disconnect() { - logger.info(`[CLUSTER]: disconnect`, this.getMeta()); - this.unbindEvents(); - this.contextHandler.stopServer(); - this.disconnected = true; - this.online = false; - this.accessible = false; - this.ready = false; - this.activated = false; - this.pushState(); - } - - @action - async refresh(opts: ClusterRefreshOptions = {}) { - logger.info(`[CLUSTER]: refresh`, this.getMeta()); - await this.whenInitialized; - await this.refreshConnectionStatus(); - if (this.accessible) { - this.isAdmin = await this.isClusterAdmin(); - await Promise.all([ - this.refreshEvents(), - this.refreshAllowedResources(), - ]); - if (opts.refreshMetadata) { - this.refreshMetadata() - } - this.ready = true - } - this.pushState(); - } - - @action - async refreshMetadata() { - logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); - const metadata = await detectorRegistry.detectForCluster(this) - const existingMetadata = this.metadata - this.metadata = Object.assign(existingMetadata, metadata) - } - - @action - async refreshConnectionStatus() { - const connectionStatus = await this.getConnectionStatus(); - this.online = connectionStatus > ClusterStatus.Offline; - this.accessible = connectionStatus == ClusterStatus.AccessGranted; - } - - @action - async refreshAllowedResources() { - this.allowedNamespaces = await this.getAllowedNamespaces(); - this.allowedResources = await this.getAllowedResources(); - } - - @action - async refreshEvents() { - this.eventCount = await this.getEventCount(); - } - protected getKubeconfig(): KubeConfig { return loadConfig(this.kubeConfigPath); } - getProxyKubeconfig(): KubeConfig { - return loadConfig(this.getProxyKubeconfigPath()); - } - - getProxyKubeconfigPath(): string { - return this.kubeconfigManager.getPath() - } - - protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { - options.headers ??= {} - options.json ??= true - options.timeout ??= 30000 - options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}` // required in ClusterManager.getClusterForRequest() - - return request(this.kubeProxyUrl + path, options) - } - - getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) { - const prometheusPrefix = this.preferences.prometheus?.prefix || ""; - const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`; - return this.k8sRequest(metricsPath, { - timeout: 0, - resolveWithFullResponse: false, - json: true, - qs: queryParams, - }) - } - - protected async getConnectionStatus(): Promise { - try { - const versionDetector = new VersionDetector(this) - const versionData = await versionDetector.detect() - this.metadata.version = versionData.value - return ClusterStatus.AccessGranted; - } catch (error) { - logger.error(`Failed to connect cluster "${this.contextName}": ${error}`) - if (error.statusCode) { - if (error.statusCode >= 400 && error.statusCode < 500) { - this.failureReason = "Invalid credentials"; - return ClusterStatus.AccessDenied; - } else { - this.failureReason = error.error || error.message; - return ClusterStatus.Offline; - } - } else if (error.failed === true) { - if (error.timedOut === true) { - this.failureReason = "Connection timed out"; - return ClusterStatus.Offline; - } else { - this.failureReason = "Failed to fetch credentials"; - return ClusterStatus.AccessDenied; - } - } - this.failureReason = error.message; - return ClusterStatus.Offline; - } - } - - async canI(resourceAttributes: V1ResourceAttributes): Promise { - const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api) - try { - const accessReview = await authApi.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { resourceAttributes } - }) - return accessReview.body.status.allowed - } catch (error) { - logger.error(`failed to request selfSubjectAccessReview: ${error}`) - return false - } - } - - async isClusterAdmin(): Promise { - return this.canI({ - namespace: "kube-system", - resource: "*", - verb: "create", - }) - } - - protected async getEventCount(): Promise { - if (!this.isAdmin) { - return 0; - } - const client = this.getProxyKubeconfig().makeApiClient(CoreV1Api); - try { - const response = await client.listEventForAllNamespaces(false, null, null, null, 1000); - const uniqEventSources = new Set(); - const warnings = response.body.items.filter(e => e.type !== 'Normal'); - for (const w of warnings) { - if (w.involvedObject.kind === 'Pod') { - try { - const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace); - logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`) - if (podHasIssues(pod)) { - uniqEventSources.add(w.involvedObject.uid); - } - } catch (err) { - } - } else { - uniqEventSources.add(w.involvedObject.uid); - } - } - const nodes = (await client.listNode()).body.items; - const nodeNotificationCount = nodes - .map(getNodeWarningConditions) - .reduce((sum, conditions) => sum + conditions.length, 0); - return uniqEventSources.size + nodeNotificationCount; - } catch (error) { - logger.error("Failed to fetch event count: " + JSON.stringify(error)) - return 0; - } - } - toJSON(): ClusterModel { const model: ClusterModel = { id: this.id, @@ -422,49 +153,5 @@ export class Cluster implements ClusterModel, ClusterState { } } - protected async getAllowedNamespaces() { - if (this.accessibleNamespaces.length) { - return this.accessibleNamespaces - } - const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api) - try { - const namespaceList = await api.listNamespace() - const nsAccessStatuses = await Promise.all( - namespaceList.body.items.map(ns => this.canI({ - namespace: ns.metadata.name, - resource: "pods", - verb: "list", - })) - ) - return namespaceList.body.items - .filter((ns, i) => nsAccessStatuses[i]) - .map(ns => ns.metadata.name) - } catch (error) { - const ctx = this.getProxyKubeconfig().getContextObject(this.contextName) - if (ctx.namespace) return [ctx.namespace] - return []; - } - } - - protected async getAllowedResources() { - try { - if (!this.allowedNamespaces.length) { - return []; - } - const resourceAccessStatuses = await Promise.all( - apiResources.map(apiResource => this.canI({ - resource: apiResource.resource, - group: apiResource.group, - verb: "list", - namespace: this.allowedNamespaces[0] - })) - ) - return apiResources - .filter((resource, i) => resourceAccessStatuses[i]) - .map(apiResource => apiResource.resource) - } catch (error) { - return [] - } - } } diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index a3cf6185dd..9192e45ec6 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -1,6 +1,6 @@ import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry" import type { ClusterPreferences } from "../common/cluster-store"; -import type { Cluster } from "./cluster" +import type { ManagedCluster } from "./managed-cluster"; import type httpProxy from "http-proxy" import url, { UrlWithStringQuery } from "url"; import { CoreV1Api } from "@kubernetes/client-node" @@ -17,9 +17,13 @@ export class ContextHandler { protected prometheusProvider: string protected prometheusPath: string - constructor(protected cluster: Cluster) { - this.clusterUrl = url.parse(cluster.apiUrl); - this.setupPrometheus(cluster.preferences); + get cluster() { + return this.managedCluster.cluster + } + + constructor(protected managedCluster: ManagedCluster) { + this.clusterUrl = url.parse(this.cluster.apiUrl); + this.setupPrometheus(this.cluster.preferences); } protected setupPrometheus(preferences: ClusterPreferences = {}) { @@ -48,7 +52,7 @@ export class ContextHandler { async getPrometheusService(): Promise { const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { - const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api) + const apiClient = this.managedCluster.getProxyKubeconfig().makeApiClient(CoreV1Api) return await provider.getPrometheusService(apiClient) }) const resolvedPrometheusServices = await Promise.all(prometheusPromises) diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 80be023227..20d160a1c5 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -3,7 +3,7 @@ import fs from "fs"; import * as yaml from "js-yaml"; import { promiseExec} from "../promise-exec" import { helmCli } from "./helm-cli"; -import { Cluster } from "../cluster"; +import { ManagedCluster } from "../managed-cluster"; import { toCamelCase } from "../../common/utils/camelCase"; export class HelmReleaseManager { @@ -48,7 +48,7 @@ export class HelmReleaseManager { } } - public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){ + public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: ManagedCluster){ const helm = await helmCli.binaryPath() const fileName = tempy.file({name: "values.yaml"}) await fs.promises.writeFile(fileName, yaml.safeDump(values)) @@ -64,7 +64,7 @@ export class HelmReleaseManager { } } - public async getRelease(name: string, namespace: string, cluster: Cluster) { + public async getRelease(name: string, namespace: string, cluster: ManagedCluster) { const helm = await helmCli.binaryPath() const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)}) const release = JSON.parse(stdout) @@ -97,10 +97,10 @@ export class HelmReleaseManager { return stdout } - protected async getResources(name: string, namespace: string, cluster: Cluster) { + protected async getResources(name: string, namespace: string, managedCluster: ManagedCluster) { const helm = await helmCli.binaryPath() - const kubectl = await cluster.kubeCtl.getPath() - const pathToKubeconfig = cluster.getProxyKubeconfigPath() + const kubectl = await managedCluster.cluster.kubeCtl.getPath() + const pathToKubeconfig = managedCluster.getProxyKubeconfigPath() 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/helm-service.ts b/src/main/helm/helm-service.ts index 664a30358c..464a89d8a0 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -1,11 +1,11 @@ -import { Cluster } from "../cluster"; +import { ManagedCluster } from "../managed-cluster"; import logger from "../logger"; import { repoManager } from "./helm-repo-manager"; import { HelmChartManager } from "./helm-chart-manager"; import { releaseManager } from "./helm-release-manager"; class HelmService { - public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { + public async installChart(cluster: ManagedCluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath()) } @@ -45,37 +45,37 @@ class HelmService { return chartManager.getValues(chartName, version) } - public async listReleases(cluster: Cluster, namespace: string = null) { + public async listReleases(cluster: ManagedCluster, namespace: string = null) { await repoManager.init() return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace) } - public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { + public async getRelease(cluster: ManagedCluster, releaseName: string, namespace: string) { logger.debug("Fetch release") return await releaseManager.getRelease(releaseName, namespace, cluster) } - public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) { + public async getReleaseValues(cluster: ManagedCluster, releaseName: string, namespace: string) { logger.debug("Fetch release values") return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath()) } - public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { + public async getReleaseHistory(cluster: ManagedCluster, releaseName: string, namespace: string) { logger.debug("Fetch release history") return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath()) } - public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) { + public async deleteRelease(cluster: ManagedCluster, releaseName: string, namespace: string) { logger.debug("Delete release") return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath()) } - public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) { + public async updateRelease(cluster: ManagedCluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) { logger.debug("Upgrade release") return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster) } - public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) { + public async rollback(cluster: ManagedCluster, releaseName: string, namespace: string, revision: number) { logger.debug("Rollback release") const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath()) return { message: output } diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 7f0b14721d..289d61f808 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -63,10 +63,10 @@ export class LensProxy { } protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { - const cluster = this.clusterManager.getClusterForRequest(req) - if (cluster) { - const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "") - const apiUrl = url.parse(cluster.apiUrl) + const managedCluster = this.clusterManager.getClusterForRequest(req) + if (managedCluster) { + const proxyUrl = await managedCluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "") + const apiUrl = url.parse(managedCluster.cluster.apiUrl) const pUrl = url.parse(proxyUrl) const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname } const proxySocket = new net.Socket() diff --git a/src/main/managed-cluster.ts b/src/main/managed-cluster.ts new file mode 100644 index 0000000000..54dd578d6d --- /dev/null +++ b/src/main/managed-cluster.ts @@ -0,0 +1,330 @@ + +import { Cluster } from "./cluster" +import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; +import { action, reaction } from "mobx"; +import { apiKubePrefix } from "../common/vars"; +import { ContextHandler } from "./context-handler" +import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" +import { Kubectl } from "./kubectl"; +import { KubeconfigManager } from "./kubeconfig-manager" +import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers" +import request, { RequestPromiseOptions } from "request-promise-native" +import { apiResources } from "../common/rbac"; +import logger from "./logger" +import { VersionDetector } from "./cluster-detectors/version-detector"; +import { detectorRegistry } from "./cluster-detectors/detector-registry"; + + +export type ClusterRefreshOptions = { + refreshMetadata?: boolean +} + +export enum ClusterStatus { + AccessGranted = 2, + AccessDenied = 1, + Offline = 0 +} + +export class ManagedCluster { + public cluster: Cluster + protected activated = false; + protected eventDisposers: Function[] = []; + public contextHandler: ContextHandler; + protected kubeconfigManager: KubeconfigManager; + + constructor(cluster: Cluster) { + this.cluster = cluster + } + + getProxyKubeconfig(): KubeConfig { + return loadConfig(this.getProxyKubeconfigPath()); + } + + getProxyKubeconfigPath(): string { + return this.kubeconfigManager.getPath() + } + + @action + async init(port: number) { + try { + this.contextHandler = new ContextHandler(this); + this.kubeconfigManager = await KubeconfigManager.create(this.cluster, this.contextHandler, port); + this.cluster.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; + this.cluster.initialized = true; + logger.info(`[CLUSTER]: "${this.cluster.contextName}" init success`, { + id: this.cluster.id, + context: this.cluster.contextName, + apiUrl: this.cluster.apiUrl + }); + } catch (err) { + logger.error(`[CLUSTER]: init failed: ${err}`, { + id: this.cluster.id, + error: err, + }); + } + } + + protected bindEvents() { + logger.info(`[CLUSTER]: bind events`, this.cluster.getMeta()) + const refreshTimer = setInterval(() => !this.cluster.disconnected && this.refresh(), 30000) // every 30s + const refreshMetadataTimer = setInterval(() => !this.cluster.disconnected && this.refreshMetadata(), 900000) // every 15 minutes + + this.eventDisposers.push( + reaction(() => this.cluster.getState(), () => this.cluster.pushState()), + () => { + clearInterval(refreshTimer) + clearInterval(refreshMetadataTimer) + }, + ); + } + + protected unbindEvents() { + logger.info(`[CLUSTER]: unbind events`, this.cluster.getMeta()); + this.eventDisposers.forEach(dispose => dispose()); + this.eventDisposers.length = 0; + } + + @action + async activate(force = false) { + if (this.activated && !force) { + return this.cluster.pushState(); + } + logger.info(`[CLUSTER]: activate`, this.cluster.getMeta()); + await this.cluster.whenInitialized; + if (!this.eventDisposers.length) { + this.bindEvents(); + } + if (this.cluster.disconnected || !this.cluster.accessible) { + await this.reconnect(); + } + await this.refreshConnectionStatus() + if (this.cluster.accessible) { + await this.refreshAllowedResources() + this.cluster.isAdmin = await this.isClusterAdmin() + this.cluster.ready = true + this.cluster.kubeCtl = new Kubectl(this.cluster.version) + this.cluster.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard + } + this.activated = true + return this.cluster.pushState(); + } + + @action + async reconnect() { + logger.info(`[CLUSTER]: reconnect`, this.cluster.getMeta()); + this.contextHandler.stopServer(); + await this.contextHandler.ensureServer(); + this.cluster.disconnected = false; + } + + @action + disconnect() { + logger.info(`[CLUSTER]: disconnect`, this.cluster.getMeta()); + this.unbindEvents(); + this.contextHandler.stopServer(); + this.cluster.disconnected = true; + this.cluster.online = false; + this.cluster.accessible = false; + this.cluster.ready = false; + this.activated = false; + this.cluster.pushState(); + } + + @action + async refresh(opts: ClusterRefreshOptions = {}) { + logger.info(`[CLUSTER]: refresh`, this.cluster.getMeta()); + await this.cluster.whenInitialized; + await this.refreshConnectionStatus(); + if (this.cluster.accessible) { + this.cluster.isAdmin = await this.isClusterAdmin(); + await Promise.all([ + this.refreshEvents(), + this.refreshAllowedResources(), + ]); + if (opts.refreshMetadata) { + this.refreshMetadata() + } + this.cluster.ready = true + } + this.cluster.pushState(); + } + + @action + async refreshMetadata() { + logger.info(`[CLUSTER]: refreshMetadata`, this.cluster.getMeta()); + const metadata = await detectorRegistry.detectForCluster(this.cluster) + const existingMetadata = this.cluster.metadata + this.cluster.metadata = Object.assign(existingMetadata, metadata) + } + + @action + async refreshConnectionStatus() { + const connectionStatus = await this.getConnectionStatus(); + this.cluster.online = connectionStatus > ClusterStatus.Offline; + this.cluster.accessible = connectionStatus == ClusterStatus.AccessGranted; + } + + @action + async refreshAllowedResources() { + this.cluster.allowedNamespaces = await this.getAllowedNamespaces(); + this.cluster.allowedResources = await this.getAllowedResources(); + } + + @action + async refreshEvents() { + this.cluster.eventCount = await this.getEventCount(); + } + + protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { + options.headers ??= {} + options.json ??= true + options.timeout ??= 30000 + options.headers.Host = `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}` // required in ClusterManager.getClusterForRequest() + + return request(this.cluster.kubeProxyUrl + path, options) + } + + getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) { + const prometheusPrefix = this.cluster.preferences.prometheus?.prefix || ""; + const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`; + return this.k8sRequest(metricsPath, { + timeout: 0, + resolveWithFullResponse: false, + json: true, + qs: queryParams, + }) + } + + protected async getConnectionStatus(): Promise { + try { + const versionDetector = new VersionDetector(this.cluster) + const versionData = await versionDetector.detect() + this.cluster.metadata.version = versionData.value + return ClusterStatus.AccessGranted; + } catch (error) { + logger.error(`Failed to connect cluster "${this.cluster.contextName}": ${error}`) + if (error.statusCode) { + if (error.statusCode >= 400 && error.statusCode < 500) { + this.cluster.failureReason = "Invalid credentials"; + return ClusterStatus.AccessDenied; + } else { + this.cluster.failureReason = error.error || error.message; + return ClusterStatus.Offline; + } + } else if (error.failed === true) { + if (error.timedOut === true) { + this.cluster.failureReason = "Connection timed out"; + return ClusterStatus.Offline; + } else { + this.cluster.failureReason = "Failed to fetch credentials"; + return ClusterStatus.AccessDenied; + } + } + this.cluster.failureReason = error.message; + return ClusterStatus.Offline; + } + } + + async canI(resourceAttributes: V1ResourceAttributes): Promise { + const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api) + try { + const accessReview = await authApi.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes } + }) + return accessReview.body.status.allowed + } catch (error) { + logger.error(`failed to request selfSubjectAccessReview: ${error}`) + return false + } + } + + async isClusterAdmin(): Promise { + return this.canI({ + namespace: "kube-system", + resource: "*", + verb: "create", + }) + } + + protected async getEventCount(): Promise { + if (!this.cluster.isAdmin) { + return 0; + } + const client = this.getProxyKubeconfig().makeApiClient(CoreV1Api); + try { + const response = await client.listEventForAllNamespaces(false, null, null, null, 1000); + const uniqEventSources = new Set(); + const warnings = response.body.items.filter(e => e.type !== 'Normal'); + for (const w of warnings) { + if (w.involvedObject.kind === 'Pod') { + try { + const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace); + logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`) + if (podHasIssues(pod)) { + uniqEventSources.add(w.involvedObject.uid); + } + } catch (err) { + } + } else { + uniqEventSources.add(w.involvedObject.uid); + } + } + const nodes = (await client.listNode()).body.items; + const nodeNotificationCount = nodes + .map(getNodeWarningConditions) + .reduce((sum, conditions) => sum + conditions.length, 0); + return uniqEventSources.size + nodeNotificationCount; + } catch (error) { + logger.error("Failed to fetch event count: " + JSON.stringify(error)) + return 0; + } + } + + protected async getAllowedNamespaces() { + if (this.cluster.accessibleNamespaces.length) { + return this.cluster.accessibleNamespaces + } + + const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api) + try { + const namespaceList = await api.listNamespace() + const nsAccessStatuses = await Promise.all( + namespaceList.body.items.map(ns => this.canI({ + namespace: ns.metadata.name, + resource: "pods", + verb: "list", + })) + ) + return namespaceList.body.items + .filter((ns, i) => nsAccessStatuses[i]) + .map(ns => ns.metadata.name) + } catch (error) { + const ctx = this.getProxyKubeconfig().getContextObject(this.cluster.contextName) + if (ctx.namespace) return [ctx.namespace] + return []; + } + } + + protected async getAllowedResources() { + try { + if (!this.cluster.allowedNamespaces.length) { + return []; + } + const resourceAccessStatuses = await Promise.all( + apiResources.map(apiResource => this.canI({ + resource: apiResource.resource, + group: apiResource.group, + verb: "list", + namespace: this.cluster.allowedNamespaces[0] + })) + ) + return apiResources + .filter((resource, i) => resourceAccessStatuses[i]) + .map(apiResource => apiResource.resource) + } catch (error) { + return [] + } + } +} diff --git a/src/main/node-shell-session.ts b/src/main/node-shell-session.ts index 9e97398327..98e32d7cfd 100644 --- a/src/main/node-shell-session.ts +++ b/src/main/node-shell-session.ts @@ -4,7 +4,7 @@ import { ShellSession } from "./shell-session"; import { v4 as uuid } from "uuid" import * as k8s from "@kubernetes/client-node" import { KubeConfig } from "@kubernetes/client-node" -import { Cluster } from "./cluster" +import { ManagedCluster } from "./managed-cluster" import logger from "./logger"; import { appEventBus } from "../common/event-bus" @@ -13,7 +13,7 @@ export class NodeShellSession extends ShellSession { protected podId: string protected kc: KubeConfig - constructor(socket: WebSocket, cluster: Cluster, nodeName: string) { + constructor(socket: WebSocket, cluster: ManagedCluster, nodeName: string) { super(socket, cluster) this.nodeName = nodeName this.podId = `node-shell-${uuid()}` @@ -133,7 +133,7 @@ export class NodeShellSession extends ShellSession { } } -export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise { +export async function openShell(socket: WebSocket, cluster: ManagedCluster, nodeName?: string): Promise { let shell: ShellSession; if (nodeName) { shell = new NodeShellSession(socket, cluster, nodeName) diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index 41441a3f90..0f7597f2c0 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -1,4 +1,4 @@ -import type { Cluster } from "./cluster"; +import type { ManagedCluster } from "./managed-cluster" import { KubernetesObject } from "@kubernetes/client-node" import { exec } from "child_process"; import fs from "fs"; @@ -10,7 +10,7 @@ import { appEventBus } from "../common/event-bus" import { cloneJsonObject } from "../common/utils"; export class ResourceApplier { - constructor(protected cluster: Cluster) { + constructor(protected managedCluster: ManagedCluster) { } async apply(resource: KubernetesObject | any): Promise { @@ -20,15 +20,15 @@ export class ResourceApplier { } protected async kubectlApply(content: string): Promise { - const { kubeCtl } = this.cluster; + const { kubeCtl } = this.managedCluster.cluster; const kubectlPath = await kubeCtl.getPath() return new Promise((resolve, reject) => { const fileName = tempy.file({ name: "resource.yaml" }) fs.writeFileSync(fileName, content) - const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"` + const cmd = `"${kubectlPath}" apply --kubeconfig "${this.managedCluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"` logger.debug("shooting manifests with: " + cmd); const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env) - const httpsProxy = this.cluster.preferences?.httpsProxy + const httpsProxy = this.managedCluster.cluster.preferences?.httpsProxy if (httpsProxy) { execEnv["HTTPS_PROXY"] = httpsProxy } @@ -46,7 +46,7 @@ export class ResourceApplier { } public async kubectlApplyAll(resources: string[]): Promise { - const { kubeCtl } = this.cluster; + const { kubeCtl } = this.managedCluster.cluster; const kubectlPath = await kubeCtl.getPath() return new Promise((resolve, reject) => { const tmpDir = tempy.directory() @@ -54,7 +54,7 @@ export class ResourceApplier { resources.forEach((resource, index) => { fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource); }) - const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"` + const cmd = `"${kubectlPath}" apply --kubeconfig "${this.managedCluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"` console.log("shooting manifests with:", cmd); exec(cmd, (error, stdout, stderr) => { if (error) { diff --git a/src/main/router.ts b/src/main/router.ts index 230c93f09e..8b6c164246 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -3,7 +3,7 @@ import Subtext from "@hapi/subtext" import http from "http" import path from "path" import { readFile } from "fs-extra" -import { Cluster } from "./cluster" +import { ManagedCluster } from "./managed-cluster" import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars"; import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes"; import logger from "./logger" @@ -11,7 +11,7 @@ import logger from "./logger" export interface RouterRequestOpts { req: http.IncomingMessage; res: http.ServerResponse; - cluster: Cluster; + cluster: ManagedCluster; params: RouteParams; url: URL; } @@ -30,7 +30,7 @@ export interface LensApiRequest

{ path: string; payload: P; params: RouteParams; - cluster: Cluster; + cluster: ManagedCluster; response: http.ServerResponse; query: URLSearchParams; raw: { @@ -46,7 +46,7 @@ export class Router { this.addRoutes() } - public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { + public async route(cluster: ManagedCluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { const url = new URL(req.url, "http://localhost"); const path = url.pathname const method = req.method.toLowerCase() diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts index 09f1f061cf..644adc2567 100644 --- a/src/main/routes/kubeconfig-route.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -1,18 +1,18 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" -import { Cluster } from "../cluster" +import { ManagedCluster } from "../managed-cluster" import { CoreV1Api, V1Secret } from "@kubernetes/client-node" -function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) { +function generateKubeConfig(username: string, secret: V1Secret, managedCluster: ManagedCluster) { const tokenData = Buffer.from(secret.data["token"], "base64") return { 'apiVersion': 'v1', 'kind': 'Config', 'clusters': [ { - 'name': cluster.contextName, + 'name': managedCluster.cluster.contextName, 'cluster': { - 'server': cluster.apiUrl, + 'server': managedCluster.cluster.apiUrl, 'certificate-authority-data': secret.data["ca.crt"] } } @@ -27,15 +27,15 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster ], 'contexts': [ { - 'name': cluster.contextName, + 'name': managedCluster.cluster.contextName, 'context': { 'user': username, - 'cluster': cluster.contextName, + 'cluster': managedCluster.cluster.contextName, 'namespace': secret.metadata.namespace, } } ], - 'current-context': cluster.contextName + 'current-context': managedCluster.cluster.contextName } } diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index dc77f7fb9f..e056f38a4a 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -1,6 +1,6 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" -import { Cluster } from "../cluster" +import { ManagedCluster } from "../managed-cluster" import _ from "lodash" export type IMetricsQuery = string | string[] | { @@ -12,7 +12,7 @@ const MAX_ATTEMPTS = 5 const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), false)), true] // prometheus metrics loader -async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record): Promise { +async function loadMetrics(promQueries: string[], cluster: ManagedCluster, prometheusPath: string, queryParams: Record): Promise { const queries = promQueries.map(p => p.trim()) const loaders = new Map>() diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 7ed79aa936..b14bdfdb1c 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -77,13 +77,13 @@ class PortForwardRoute extends LensApi { const { namespace, port, resourceType, resourceName } = params let portForward = PortForward.getPortforward({ - clusterId: cluster.id, kind: resourceType, name: resourceName, + clusterId: cluster.cluster.id, kind: resourceType, name: resourceName, namespace: namespace, port: port }) if (!portForward) { logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`) portForward = new PortForward({ - clusterId: cluster.id, + clusterId: cluster.cluster.id, kind: resourceType, namespace: namespace, name: resourceName, diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 6ea9e4eede..739db8438e 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -5,7 +5,7 @@ import path from "path" import shellEnv from "shell-env" import { app } from "electron" import { Kubectl } from "./kubectl" -import { Cluster } from "./cluster" +import { ManagedCluster } from "./managed-cluster" import { ClusterPreferences } from "../common/cluster-store"; import { helmCli } from "./helm/helm-cli" import { isWindows } from "../common/vars"; @@ -27,13 +27,13 @@ export class ShellSession extends EventEmitter { protected running = false; protected clusterId: string; - constructor(socket: WebSocket, cluster: Cluster) { + constructor(socket: WebSocket, managedCluster: ManagedCluster) { super() this.websocket = socket - this.kubeconfigPath = cluster.getProxyKubeconfigPath() - this.kubectl = new Kubectl(cluster.version) - this.preferences = cluster.preferences || {} - this.clusterId = cluster.id + this.kubeconfigPath = managedCluster.getProxyKubeconfigPath() + this.kubectl = new Kubectl(managedCluster.cluster.version) + this.preferences = managedCluster.cluster.preferences || {} + this.clusterId = managedCluster.cluster.id } public async open() {