1
0
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:
Jari Kolehmainen 2020-11-18 17:28:45 +02:00
parent a731e66b18
commit 374f066b86
19 changed files with 608 additions and 477 deletions

View File

@ -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)

View File

@ -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)
}

View File

@ -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()
})
})

View File

@ -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)

View 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()
})
})

View File

@ -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;

View File

@ -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 []
}
}
}

View File

@ -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)

View File

@ -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: []})}
})

View File

@ -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 }

View File

@ -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
View 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 []
}
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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()

View File

@ -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
}
}

View File

@ -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>>()

View File

@ -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,

View File

@ -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() {