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 { appEventBus } from "./event-bus"
|
||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
import { ClusterManager } from "../main/cluster-manager";
|
||||||
|
|
||||||
export const clusterActivateHandler = "cluster:activate"
|
export const clusterActivateHandler = "cluster:activate"
|
||||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id"
|
export const clusterSetFrameIdHandler = "cluster:set-frame-id"
|
||||||
@ -10,35 +11,35 @@ export const clusterRefreshHandler = "cluster:refresh"
|
|||||||
export const clusterDisconnectHandler = "cluster:disconnect"
|
export const clusterDisconnectHandler = "cluster:disconnect"
|
||||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"
|
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"
|
||||||
|
|
||||||
|
function getById(clusterId: ClusterId) {
|
||||||
|
return ClusterManager.getInstance<ClusterManager>().getClusterById(clusterId)
|
||||||
|
}
|
||||||
|
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
return getById(clusterId)?.activate(force);
|
||||||
if (cluster) {
|
|
||||||
return cluster.activate(force);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId?: number) => {
|
handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId?: number) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const managedCluster = getById(clusterId);
|
||||||
if (cluster) {
|
if (managedCluster) {
|
||||||
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
if (frameId) managedCluster.cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||||
return cluster.pushState();
|
return managedCluster.cluster.pushState();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
getById(clusterId)?.refresh({ refreshMetadata: true });
|
||||||
if (cluster) return cluster.refresh({ refreshMetadata: true })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
||||||
appEventBus.emit({name: "cluster", action: "stop"});
|
appEventBus.emit({name: "cluster", action: "stop"});
|
||||||
return clusterStore.getById(clusterId)?.disconnect();
|
return getById(clusterId)?.disconnect();
|
||||||
})
|
})
|
||||||
|
|
||||||
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
||||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
|
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = getById(clusterId);
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const applier = new ResourceApplier(cluster)
|
const applier = new ResourceApplier(cluster)
|
||||||
applier.kubectlApplyAll(resources)
|
applier.kubectlApplyAll(resources)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import logger from "../main/logger";
|
|||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
import { requestMain } from "../common/ipc";
|
import { requestMain } from "../common/ipc";
|
||||||
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
||||||
|
import { ClusterManager } from "../main/cluster-manager";
|
||||||
|
|
||||||
export interface ClusterFeatureStatus {
|
export interface ClusterFeatureStatus {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
@ -38,7 +39,8 @@ export abstract class ClusterFeature {
|
|||||||
|
|
||||||
protected async applyResources(cluster: Cluster, resources: string[]) {
|
protected async applyResources(cluster: Cluster, resources: string[]) {
|
||||||
if (app) {
|
if (app) {
|
||||||
await new ResourceApplier(cluster).kubectlApplyAll(resources)
|
const managedCluster = ClusterManager.getInstance<ClusterManager>().getClusterById(cluster.id)
|
||||||
|
await new ResourceApplier(managedCluster).kubectlApplyAll(resources)
|
||||||
} else {
|
} else {
|
||||||
await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources)
|
await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,10 +33,6 @@ import { Console } from "console";
|
|||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { workspaceStore } from "../../common/workspace-store";
|
import { workspaceStore } from "../../common/workspace-store";
|
||||||
import { Cluster } from "../cluster"
|
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 request from "request-promise-native"
|
||||||
import { Kubectl } from "../kubectl";
|
import { Kubectl } from "../kubectl";
|
||||||
|
|
||||||
@ -90,78 +86,4 @@ describe("create clusters", () => {
|
|||||||
})
|
})
|
||||||
expect(c.apiUrl).toBe("https://192.168.64.3:8443")
|
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 { KubeconfigManager } from "../kubeconfig-manager"
|
||||||
import mockFs from "mock-fs"
|
import mockFs from "mock-fs"
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
|
import { ManagedCluster } from "../managed-cluster";
|
||||||
import { workspaceStore } from "../../common/workspace-store";
|
import { workspaceStore } from "../../common/workspace-store";
|
||||||
import { ContextHandler } from "../context-handler";
|
import { ContextHandler } from "../context-handler";
|
||||||
import { getFreePort } from "../port";
|
import { getFreePort } from "../port";
|
||||||
@ -78,7 +79,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml",
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
workspace: workspaceStore.currentWorkspaceId
|
||||||
})
|
})
|
||||||
const contextHandler = new ContextHandler(cluster)
|
const contextHandler = new ContextHandler(new ManagedCluster(cluster))
|
||||||
const port = await getFreePort()
|
const port = await getFreePort()
|
||||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
|
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml",
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
workspace: workspaceStore.currentWorkspaceId
|
||||||
})
|
})
|
||||||
const contextHandler = new ContextHandler(cluster)
|
const contextHandler = new ContextHandler(new ManagedCluster(cluster))
|
||||||
const port = await getFreePort()
|
const port = await getFreePort()
|
||||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
|
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 type http from "http"
|
||||||
import { ipcMain } from "electron"
|
import { ipcMain } from "electron"
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"
|
import { ClusterId, clusterStore, getClusterIdFromHost } from "../common/cluster-store"
|
||||||
import { Cluster } from "./cluster"
|
import { Cluster } from "./cluster"
|
||||||
|
import { ManagedCluster } from "./managed-cluster"
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
|
|
||||||
export class ClusterManager extends Singleton {
|
export class ClusterManager extends Singleton {
|
||||||
|
protected managedClusters: Map<ClusterId, ManagedCluster> = new Map()
|
||||||
|
|
||||||
|
|
||||||
constructor(public readonly port: number) {
|
constructor(public readonly port: number) {
|
||||||
super()
|
super()
|
||||||
// auto-init clusters
|
// auto-init clusters
|
||||||
@ -16,7 +20,9 @@ export class ClusterManager extends Singleton {
|
|||||||
clusterStore.enabledClustersList.forEach(cluster => {
|
clusterStore.enabledClustersList.forEach(cluster => {
|
||||||
if (!cluster.initialized) {
|
if (!cluster.initialized) {
|
||||||
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
|
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) {
|
if (removedClusters.length > 0) {
|
||||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
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();
|
clusterStore.removedClusters.clear();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
@ -44,7 +55,10 @@ export class ClusterManager extends Singleton {
|
|||||||
if (!cluster.disconnected) {
|
if (!cluster.disconnected) {
|
||||||
cluster.online = false
|
cluster.online = false
|
||||||
cluster.accessible = 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")
|
logger.info("[CLUSTER-MANAGER]: network is online")
|
||||||
clusterStore.enabledClustersList.forEach((cluster) => {
|
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||||
if (!cluster.disconnected) {
|
if (!cluster.disconnected) {
|
||||||
cluster.refreshConnectionStatus().catch((e) => e)
|
const managedCluster = this.managedClusters.get(cluster.id)
|
||||||
|
if (managedCluster) {
|
||||||
|
managedCluster.refreshConnectionStatus().catch((e) => e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
clusterStore.clusters.forEach((cluster: Cluster) => {
|
clusterStore.clusters.forEach((cluster: Cluster) => {
|
||||||
cluster.disconnect();
|
const managedCluster = this.managedClusters.get(cluster.id)
|
||||||
|
if (managedCluster) {
|
||||||
|
managedCluster.disconnect()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
getClusterById(id: ClusterId) {
|
||||||
let cluster: Cluster = null
|
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>
|
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||||
const clusterId = req.url.split("/")[1]
|
const clusterId = req.url.split("/")[1]
|
||||||
cluster = clusterStore.getById(clusterId)
|
cluster = this.managedClusters.get(clusterId)
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
// we need to swap path prefix so that request is proxied to kube api
|
// we need to swap path prefix so that request is proxied to kube api
|
||||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
|
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
|
||||||
}
|
}
|
||||||
} else if (req.headers["x-cluster-id"]) {
|
} 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 {
|
} else {
|
||||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||||
cluster = clusterStore.getById(clusterId)
|
cluster = this.managedClusters.get(clusterId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cluster;
|
return cluster;
|
||||||
|
|||||||
@ -1,26 +1,11 @@
|
|||||||
import { ipcMain } from "electron"
|
|
||||||
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
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 type { WorkspaceId } from "../common/workspace-store";
|
||||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, computed, observable, toJS, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
|
||||||
import { broadcastMessage } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import { ContextHandler } from "./context-handler"
|
import { KubeConfig } from "@kubernetes/client-node"
|
||||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
|
||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
import { loadConfig } from "../common/kube-helpers"
|
||||||
import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers"
|
import logger from "./logger";
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ClusterMetadataKey {
|
export enum ClusterMetadataKey {
|
||||||
VERSION = "version",
|
VERSION = "version",
|
||||||
@ -30,9 +15,7 @@ export enum ClusterMetadataKey {
|
|||||||
LAST_SEEN = "lastSeen"
|
LAST_SEEN = "lastSeen"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClusterRefreshOptions = {
|
|
||||||
refreshMetadata?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClusterState {
|
export interface ClusterState {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
@ -52,11 +35,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
public id: ClusterId;
|
public id: ClusterId;
|
||||||
public frameId: number;
|
public frameId: number;
|
||||||
public kubeCtl: Kubectl
|
public kubeCtl: Kubectl
|
||||||
public contextHandler: ContextHandler;
|
|
||||||
public ownerRef: string;
|
public ownerRef: string;
|
||||||
protected kubeconfigManager: KubeconfigManager;
|
|
||||||
protected eventDisposers: Function[] = [];
|
|
||||||
protected activated = false;
|
|
||||||
|
|
||||||
whenInitialized = when(() => this.initialized);
|
whenInitialized = when(() => this.initialized);
|
||||||
whenReady = when(() => this.ready);
|
whenReady = when(() => this.ready);
|
||||||
@ -111,258 +90,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
Object.assign(this, model);
|
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 {
|
protected getKubeconfig(): KubeConfig {
|
||||||
return loadConfig(this.kubeConfigPath);
|
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 {
|
toJSON(): ClusterModel {
|
||||||
const model: ClusterModel = {
|
const model: ClusterModel = {
|
||||||
id: this.id,
|
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 { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
|
||||||
import type { ClusterPreferences } from "../common/cluster-store";
|
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 type httpProxy from "http-proxy"
|
||||||
import url, { UrlWithStringQuery } from "url";
|
import url, { UrlWithStringQuery } from "url";
|
||||||
import { CoreV1Api } from "@kubernetes/client-node"
|
import { CoreV1Api } from "@kubernetes/client-node"
|
||||||
@ -17,9 +17,13 @@ export class ContextHandler {
|
|||||||
protected prometheusProvider: string
|
protected prometheusProvider: string
|
||||||
protected prometheusPath: string
|
protected prometheusPath: string
|
||||||
|
|
||||||
constructor(protected cluster: Cluster) {
|
get cluster() {
|
||||||
this.clusterUrl = url.parse(cluster.apiUrl);
|
return this.managedCluster.cluster
|
||||||
this.setupPrometheus(cluster.preferences);
|
}
|
||||||
|
|
||||||
|
constructor(protected managedCluster: ManagedCluster) {
|
||||||
|
this.clusterUrl = url.parse(this.cluster.apiUrl);
|
||||||
|
this.setupPrometheus(this.cluster.preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupPrometheus(preferences: ClusterPreferences = {}) {
|
protected setupPrometheus(preferences: ClusterPreferences = {}) {
|
||||||
@ -48,7 +52,7 @@ export class ContextHandler {
|
|||||||
async getPrometheusService(): Promise<PrometheusService> {
|
async getPrometheusService(): Promise<PrometheusService> {
|
||||||
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
||||||
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
||||||
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api)
|
const apiClient = this.managedCluster.getProxyKubeconfig().makeApiClient(CoreV1Api)
|
||||||
return await provider.getPrometheusService(apiClient)
|
return await provider.getPrometheusService(apiClient)
|
||||||
})
|
})
|
||||||
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
|
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import fs from "fs";
|
|||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import { promiseExec} from "../promise-exec"
|
import { promiseExec} from "../promise-exec"
|
||||||
import { helmCli } from "./helm-cli";
|
import { helmCli } from "./helm-cli";
|
||||||
import { Cluster } from "../cluster";
|
import { ManagedCluster } from "../managed-cluster";
|
||||||
import { toCamelCase } from "../../common/utils/camelCase";
|
import { toCamelCase } from "../../common/utils/camelCase";
|
||||||
|
|
||||||
export class HelmReleaseManager {
|
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 helm = await helmCli.binaryPath()
|
||||||
const fileName = tempy.file({name: "values.yaml"})
|
const fileName = tempy.file({name: "values.yaml"})
|
||||||
await fs.promises.writeFile(fileName, yaml.safeDump(values))
|
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 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 {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)})
|
||||||
const release = JSON.parse(stdout)
|
const release = JSON.parse(stdout)
|
||||||
@ -97,10 +97,10 @@ export class HelmReleaseManager {
|
|||||||
return stdout
|
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 helm = await helmCli.binaryPath()
|
||||||
const kubectl = await cluster.kubeCtl.getPath()
|
const kubectl = await managedCluster.cluster.kubeCtl.getPath()
|
||||||
const pathToKubeconfig = cluster.getProxyKubeconfigPath()
|
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) => {
|
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => {
|
||||||
return { stdout: JSON.stringify({items: []})}
|
return { stdout: JSON.stringify({items: []})}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Cluster } from "../cluster";
|
import { ManagedCluster } from "../managed-cluster";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { repoManager } from "./helm-repo-manager";
|
import { repoManager } from "./helm-repo-manager";
|
||||||
import { HelmChartManager } from "./helm-chart-manager";
|
import { HelmChartManager } from "./helm-chart-manager";
|
||||||
import { releaseManager } from "./helm-release-manager";
|
import { releaseManager } from "./helm-release-manager";
|
||||||
|
|
||||||
class HelmService {
|
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())
|
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)
|
return chartManager.getValues(chartName, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
public async listReleases(cluster: ManagedCluster, namespace: string = null) {
|
||||||
await repoManager.init()
|
await repoManager.init()
|
||||||
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace)
|
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")
|
logger.debug("Fetch release")
|
||||||
return await releaseManager.getRelease(releaseName, namespace, cluster)
|
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")
|
logger.debug("Fetch release values")
|
||||||
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath())
|
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")
|
logger.debug("Fetch release history")
|
||||||
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath())
|
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")
|
logger.debug("Delete release")
|
||||||
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath())
|
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")
|
logger.debug("Upgrade release")
|
||||||
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster)
|
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")
|
logger.debug("Rollback release")
|
||||||
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath())
|
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath())
|
||||||
return { message: output }
|
return { message: output }
|
||||||
|
|||||||
@ -63,10 +63,10 @@ export class LensProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
const managedCluster = this.clusterManager.getClusterForRequest(req)
|
||||||
if (cluster) {
|
if (managedCluster) {
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "")
|
const proxyUrl = await managedCluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "")
|
||||||
const apiUrl = url.parse(cluster.apiUrl)
|
const apiUrl = url.parse(managedCluster.cluster.apiUrl)
|
||||||
const pUrl = url.parse(proxyUrl)
|
const pUrl = url.parse(proxyUrl)
|
||||||
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }
|
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }
|
||||||
const proxySocket = new net.Socket()
|
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 { v4 as uuid } from "uuid"
|
||||||
import * as k8s from "@kubernetes/client-node"
|
import * as k8s from "@kubernetes/client-node"
|
||||||
import { KubeConfig } from "@kubernetes/client-node"
|
import { KubeConfig } from "@kubernetes/client-node"
|
||||||
import { Cluster } from "./cluster"
|
import { ManagedCluster } from "./managed-cluster"
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { appEventBus } from "../common/event-bus"
|
import { appEventBus } from "../common/event-bus"
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
protected podId: string
|
protected podId: string
|
||||||
protected kc: KubeConfig
|
protected kc: KubeConfig
|
||||||
|
|
||||||
constructor(socket: WebSocket, cluster: Cluster, nodeName: string) {
|
constructor(socket: WebSocket, cluster: ManagedCluster, nodeName: string) {
|
||||||
super(socket, cluster)
|
super(socket, cluster)
|
||||||
this.nodeName = nodeName
|
this.nodeName = nodeName
|
||||||
this.podId = `node-shell-${uuid()}`
|
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;
|
let shell: ShellSession;
|
||||||
if (nodeName) {
|
if (nodeName) {
|
||||||
shell = new NodeShellSession(socket, cluster, 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 { KubernetesObject } from "@kubernetes/client-node"
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@ -10,7 +10,7 @@ import { appEventBus } from "../common/event-bus"
|
|||||||
import { cloneJsonObject } from "../common/utils";
|
import { cloneJsonObject } from "../common/utils";
|
||||||
|
|
||||||
export class ResourceApplier {
|
export class ResourceApplier {
|
||||||
constructor(protected cluster: Cluster) {
|
constructor(protected managedCluster: ManagedCluster) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||||
@ -20,15 +20,15 @@ export class ResourceApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async kubectlApply(content: string): Promise<string> {
|
protected async kubectlApply(content: string): Promise<string> {
|
||||||
const { kubeCtl } = this.cluster;
|
const { kubeCtl } = this.managedCluster.cluster;
|
||||||
const kubectlPath = await kubeCtl.getPath()
|
const kubectlPath = await kubeCtl.getPath()
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
const fileName = tempy.file({ name: "resource.yaml" })
|
const fileName = tempy.file({ name: "resource.yaml" })
|
||||||
fs.writeFileSync(fileName, content)
|
fs.writeFileSync(fileName, content)
|
||||||
const cmd = `"${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);
|
logger.debug("shooting manifests with: " + cmd);
|
||||||
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env)
|
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env)
|
||||||
const httpsProxy = this.cluster.preferences?.httpsProxy
|
const httpsProxy = this.managedCluster.cluster.preferences?.httpsProxy
|
||||||
if (httpsProxy) {
|
if (httpsProxy) {
|
||||||
execEnv["HTTPS_PROXY"] = httpsProxy
|
execEnv["HTTPS_PROXY"] = httpsProxy
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ export class ResourceApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async kubectlApplyAll(resources: string[]): Promise<string> {
|
public async kubectlApplyAll(resources: string[]): Promise<string> {
|
||||||
const { kubeCtl } = this.cluster;
|
const { kubeCtl } = this.managedCluster.cluster;
|
||||||
const kubectlPath = await kubeCtl.getPath()
|
const kubectlPath = await kubeCtl.getPath()
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tmpDir = tempy.directory()
|
const tmpDir = tempy.directory()
|
||||||
@ -54,7 +54,7 @@ export class ResourceApplier {
|
|||||||
resources.forEach((resource, index) => {
|
resources.forEach((resource, index) => {
|
||||||
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
|
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);
|
console.log("shooting manifests with:", cmd);
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
exec(cmd, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Subtext from "@hapi/subtext"
|
|||||||
import http from "http"
|
import http from "http"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { readFile } from "fs-extra"
|
import { readFile } from "fs-extra"
|
||||||
import { Cluster } from "./cluster"
|
import { ManagedCluster } from "./managed-cluster"
|
||||||
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
||||||
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
@ -11,7 +11,7 @@ import logger from "./logger"
|
|||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
req: http.IncomingMessage;
|
req: http.IncomingMessage;
|
||||||
res: http.ServerResponse;
|
res: http.ServerResponse;
|
||||||
cluster: Cluster;
|
cluster: ManagedCluster;
|
||||||
params: RouteParams;
|
params: RouteParams;
|
||||||
url: URL;
|
url: URL;
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ export interface LensApiRequest<P = any> {
|
|||||||
path: string;
|
path: string;
|
||||||
payload: P;
|
payload: P;
|
||||||
params: RouteParams;
|
params: RouteParams;
|
||||||
cluster: Cluster;
|
cluster: ManagedCluster;
|
||||||
response: http.ServerResponse;
|
response: http.ServerResponse;
|
||||||
query: URLSearchParams;
|
query: URLSearchParams;
|
||||||
raw: {
|
raw: {
|
||||||
@ -46,7 +46,7 @@ export class Router {
|
|||||||
this.addRoutes()
|
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 url = new URL(req.url, "http://localhost");
|
||||||
const path = url.pathname
|
const path = url.pathname
|
||||||
const method = req.method.toLowerCase()
|
const method = req.method.toLowerCase()
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { LensApiRequest } from "../router"
|
import { LensApiRequest } from "../router"
|
||||||
import { LensApi } from "../lens-api"
|
import { LensApi } from "../lens-api"
|
||||||
import { Cluster } from "../cluster"
|
import { ManagedCluster } from "../managed-cluster"
|
||||||
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
|
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")
|
const tokenData = Buffer.from(secret.data["token"], "base64")
|
||||||
return {
|
return {
|
||||||
'apiVersion': 'v1',
|
'apiVersion': 'v1',
|
||||||
'kind': 'Config',
|
'kind': 'Config',
|
||||||
'clusters': [
|
'clusters': [
|
||||||
{
|
{
|
||||||
'name': cluster.contextName,
|
'name': managedCluster.cluster.contextName,
|
||||||
'cluster': {
|
'cluster': {
|
||||||
'server': cluster.apiUrl,
|
'server': managedCluster.cluster.apiUrl,
|
||||||
'certificate-authority-data': secret.data["ca.crt"]
|
'certificate-authority-data': secret.data["ca.crt"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,15 +27,15 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
|
|||||||
],
|
],
|
||||||
'contexts': [
|
'contexts': [
|
||||||
{
|
{
|
||||||
'name': cluster.contextName,
|
'name': managedCluster.cluster.contextName,
|
||||||
'context': {
|
'context': {
|
||||||
'user': username,
|
'user': username,
|
||||||
'cluster': cluster.contextName,
|
'cluster': managedCluster.cluster.contextName,
|
||||||
'namespace': secret.metadata.namespace,
|
'namespace': secret.metadata.namespace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'current-context': cluster.contextName
|
'current-context': managedCluster.cluster.contextName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { LensApiRequest } from "../router"
|
import { LensApiRequest } from "../router"
|
||||||
import { LensApi } from "../lens-api"
|
import { LensApi } from "../lens-api"
|
||||||
import { Cluster } from "../cluster"
|
import { ManagedCluster } from "../managed-cluster"
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
|
|
||||||
export type IMetricsQuery = string | string[] | {
|
export type IMetricsQuery = string | string[] | {
|
||||||
@ -12,7 +12,7 @@ const MAX_ATTEMPTS = 5
|
|||||||
const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), false)), true]
|
const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), false)), true]
|
||||||
|
|
||||||
// prometheus metrics loader
|
// 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 queries = promQueries.map(p => p.trim())
|
||||||
const loaders = new Map<string, Promise<any>>()
|
const loaders = new Map<string, Promise<any>>()
|
||||||
|
|
||||||
|
|||||||
@ -77,13 +77,13 @@ class PortForwardRoute extends LensApi {
|
|||||||
const { namespace, port, resourceType, resourceName } = params
|
const { namespace, port, resourceType, resourceName } = params
|
||||||
|
|
||||||
let portForward = PortForward.getPortforward({
|
let portForward = PortForward.getPortforward({
|
||||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
clusterId: cluster.cluster.id, kind: resourceType, name: resourceName,
|
||||||
namespace: namespace, port: port
|
namespace: namespace, port: port
|
||||||
})
|
})
|
||||||
if (!portForward) {
|
if (!portForward) {
|
||||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`)
|
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`)
|
||||||
portForward = new PortForward({
|
portForward = new PortForward({
|
||||||
clusterId: cluster.id,
|
clusterId: cluster.cluster.id,
|
||||||
kind: resourceType,
|
kind: resourceType,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
name: resourceName,
|
name: resourceName,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import path from "path"
|
|||||||
import shellEnv from "shell-env"
|
import shellEnv from "shell-env"
|
||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
import { Kubectl } from "./kubectl"
|
import { Kubectl } from "./kubectl"
|
||||||
import { Cluster } from "./cluster"
|
import { ManagedCluster } from "./managed-cluster"
|
||||||
import { ClusterPreferences } from "../common/cluster-store";
|
import { ClusterPreferences } from "../common/cluster-store";
|
||||||
import { helmCli } from "./helm/helm-cli"
|
import { helmCli } from "./helm/helm-cli"
|
||||||
import { isWindows } from "../common/vars";
|
import { isWindows } from "../common/vars";
|
||||||
@ -27,13 +27,13 @@ export class ShellSession extends EventEmitter {
|
|||||||
protected running = false;
|
protected running = false;
|
||||||
protected clusterId: string;
|
protected clusterId: string;
|
||||||
|
|
||||||
constructor(socket: WebSocket, cluster: Cluster) {
|
constructor(socket: WebSocket, managedCluster: ManagedCluster) {
|
||||||
super()
|
super()
|
||||||
this.websocket = socket
|
this.websocket = socket
|
||||||
this.kubeconfigPath = cluster.getProxyKubeconfigPath()
|
this.kubeconfigPath = managedCluster.getProxyKubeconfigPath()
|
||||||
this.kubectl = new Kubectl(cluster.version)
|
this.kubectl = new Kubectl(managedCluster.cluster.version)
|
||||||
this.preferences = cluster.preferences || {}
|
this.preferences = managedCluster.cluster.preferences || {}
|
||||||
this.clusterId = cluster.id
|
this.clusterId = managedCluster.cluster.id
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user