mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
split cluster to managed cluster
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
a731e66b18
commit
374f066b86
@ -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<ClusterManager>().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)
|
||||
|
||||
@ -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<ClusterManager>().getClusterById(cluster.id)
|
||||
await new ResourceApplier(managedCluster).kubectlApplyAll(resources)
|
||||
} else {
|
||||
await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources)
|
||||
}
|
||||
|
||||
@ -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<boolean> => {
|
||||
expect(attr.namespace).toBe("default")
|
||||
expect(attr.resource).toBe("pods")
|
||||
expect(attr.verb).toBe("list")
|
||||
return Promise.resolve(true)
|
||||
})
|
||||
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
160
src/main/__test__/managed-cluster.test.ts
Normal file
160
src/main/__test__/managed-cluster.test.ts
Normal file
@ -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<typeof request>
|
||||
|
||||
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<boolean> => {
|
||||
expect(attr.namespace).toBe("default")
|
||||
expect(attr.resource).toBe("pods")
|
||||
expect(attr.verb).toBe("list")
|
||||
return Promise.resolve(true)
|
||||
})
|
||||
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
@ -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<ClusterId, ManagedCluster> = 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:<port>/<uid>
|
||||
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;
|
||||
|
||||
@ -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<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
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<ClusterStatus> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
return this.canI({
|
||||
namespace: "kube-system",
|
||||
resource: "*",
|
||||
verb: "create",
|
||||
})
|
||||
}
|
||||
|
||||
protected async getEventCount(): Promise<number> {
|
||||
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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PrometheusService> {
|
||||
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
||||
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
||||
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)
|
||||
|
||||
@ -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: []})}
|
||||
})
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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()
|
||||
|
||||
330
src/main/managed-cluster.ts
Normal file
330
src/main/managed-cluster.ts
Normal file
@ -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<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
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<ClusterStatus> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
return this.canI({
|
||||
namespace: "kube-system",
|
||||
resource: "*",
|
||||
verb: "create",
|
||||
})
|
||||
}
|
||||
|
||||
protected async getEventCount(): Promise<number> {
|
||||
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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<ShellSession> {
|
||||
export async function openShell(socket: WebSocket, cluster: ManagedCluster, nodeName?: string): Promise<ShellSession> {
|
||||
let shell: ShellSession;
|
||||
if (nodeName) {
|
||||
shell = new NodeShellSession(socket, cluster, nodeName)
|
||||
|
||||
@ -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<string> {
|
||||
@ -20,15 +20,15 @@ export class ResourceApplier {
|
||||
}
|
||||
|
||||
protected async kubectlApply(content: string): Promise<string> {
|
||||
const { kubeCtl } = this.cluster;
|
||||
const { kubeCtl } = this.managedCluster.cluster;
|
||||
const kubectlPath = await kubeCtl.getPath()
|
||||
return new Promise<string>((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<string> {
|
||||
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) {
|
||||
|
||||
@ -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<P = any> {
|
||||
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<boolean> {
|
||||
public async route(cluster: ManagedCluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
const path = url.pathname
|
||||
const method = req.method.toLowerCase()
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<string, string>): Promise<any[]> {
|
||||
async function loadMetrics(promQueries: string[], cluster: ManagedCluster, prometheusPath: string, queryParams: Record<string, string>): Promise<any[]> {
|
||||
const queries = promQueries.map(p => p.trim())
|
||||
const loaders = new Map<string, Promise<any>>()
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user