mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add context menu entry for deleting local clusters (#2923)
This commit is contained in:
1 parent
e47d26e1ce
commit
eb45f45a7a
30 files changed
+348
-377
No files matched your search
@@ -120,8 +120,8 @@ describe("create clusters", () => {
|
||||
protected bindEvents() {
|
||||
return;
|
||||
}
|
||||
protected async ensureKubectl() {
|
||||
return Promise.resolve(true);
|
||||
async ensureKubectl() {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}({
|
||||
id: "foo",
|
||||
|
||||
@@ -30,7 +30,7 @@ import type { CatalogEntity } from "../common/catalog";
|
||||
|
||||
const broadcaster = debounce((items: CatalogEntity[]) => {
|
||||
broadcastMessage("catalog:items", items);
|
||||
}, 1_000, { trailing: true });
|
||||
}, 1_000, { leading: true, trailing: true });
|
||||
|
||||
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
|
||||
return reaction(() => toJS(catalog.items), (items) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { computeDiff, configToModels } from "../kubeconfig-sync";
|
||||
import mockFs from "mock-fs";
|
||||
import fs from "fs";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@@ -38,10 +39,13 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
beforeEach(() => {
|
||||
mockFs();
|
||||
ClusterStore.createInstance();
|
||||
ClusterManager.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
ClusterStore.resetInstance();
|
||||
ClusterManager.resetInstance();
|
||||
});
|
||||
|
||||
describe("configsToModels", () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ import logger from "../logger";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { loadConfigFromString, splitConfig } from "../../common/kube-helpers";
|
||||
import { Cluster } from "../cluster";
|
||||
import { catalogEntityFromCluster } from "../cluster-manager";
|
||||
import { catalogEntityFromCluster, ClusterManager } from "../cluster-manager";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
|
||||
import { createHash } from "crypto";
|
||||
@@ -170,6 +170,9 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
||||
|
||||
// remove and disconnect clusters that were removed from the config
|
||||
if (!model) {
|
||||
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
|
||||
ClusterManager.getInstance().deleting.delete(value[0].id);
|
||||
|
||||
value[0].disconnect();
|
||||
source.delete(contextName);
|
||||
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
import "../common/cluster-ipc";
|
||||
import type http from "http";
|
||||
import { action, autorun, makeObservable, reaction, toJS } from "mobx";
|
||||
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||
import type { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
@@ -30,17 +30,18 @@ import { Singleton } from "../common/utils";
|
||||
import { catalogEntityRegistry } from "./catalog";
|
||||
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { once } from "lodash";
|
||||
|
||||
export class ClusterManager extends Singleton {
|
||||
private store = ClusterStore.getInstance();
|
||||
deleting = observable.set<ClusterId>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
makeObservable(this);
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
init = once(() => {
|
||||
// reacting to every cluster's state change and total amount of items
|
||||
reaction(
|
||||
() => this.store.clustersList.map(c => c.getState()),
|
||||
@@ -59,6 +60,12 @@ export class ClusterManager extends Singleton {
|
||||
this.syncClustersFromCatalog(entities);
|
||||
});
|
||||
|
||||
observe(this.deleting, change => {
|
||||
if (change.type === "add") {
|
||||
this.updateEntityStatus(catalogEntityRegistry.getById(change.newValue));
|
||||
}
|
||||
});
|
||||
|
||||
// auto-stop removed clusters
|
||||
autorun(() => {
|
||||
const removedClusters = Array.from(this.store.removedClusters.values());
|
||||
@@ -76,7 +83,7 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
ipcMainOn("network:offline", this.onNetworkOffline);
|
||||
ipcMainOn("network:online", this.onNetworkOnline);
|
||||
}
|
||||
});
|
||||
|
||||
@action
|
||||
protected updateCatalog(clusters: Cluster[]) {
|
||||
@@ -115,8 +122,15 @@ export class ClusterManager extends Singleton {
|
||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||
}
|
||||
|
||||
protected updateEntityStatus(entity: KubernetesCluster, cluster: Cluster) {
|
||||
entity.status.phase = cluster.accessible ? "connected" : "disconnected";
|
||||
@action
|
||||
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
|
||||
if (this.deleting.has(entity.getId())) {
|
||||
entity.status.phase = "deleting";
|
||||
entity.status.enabled = false;
|
||||
} else {
|
||||
entity.status.phase = cluster?.accessible ? "connected" : "disconnected";
|
||||
entity.status.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||
|
||||
+8
-11
@@ -88,12 +88,7 @@ export interface ClusterState {
|
||||
export class Cluster implements ClusterModel, ClusterState {
|
||||
/** Unique id for a cluster */
|
||||
public readonly id: ClusterId;
|
||||
/**
|
||||
* Kubectl
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public kubeCtl: Kubectl;
|
||||
private kubeCtl: Kubectl;
|
||||
/**
|
||||
* Context handler
|
||||
*
|
||||
@@ -363,7 +358,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
|
||||
if (this.accessible) {
|
||||
await this.refreshAccessibility();
|
||||
this.ensureKubectl();
|
||||
this.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
||||
}
|
||||
this.activated = true;
|
||||
|
||||
@@ -373,10 +368,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async ensureKubectl() {
|
||||
this.kubeCtl = new Kubectl(this.version);
|
||||
async ensureKubectl() {
|
||||
this.kubeCtl ??= new Kubectl(this.version);
|
||||
|
||||
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
||||
await this.kubeCtl.ensureKubectl();
|
||||
|
||||
return this.kubeCtl;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -650,7 +647,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
|
||||
try {
|
||||
const { body: { items }} = await api.listNamespace();
|
||||
const { body: { items } } = await api.listNamespace();
|
||||
const namespaces = items.map(ns => ns.metadata.name);
|
||||
|
||||
this.getAllowedNamespacesErrorCount = 0; // reset on success
|
||||
|
||||
@@ -169,9 +169,10 @@ export async function rollback(name: string, namespace: string, revision: number
|
||||
async function getResources(name: string, namespace: string, cluster: Cluster) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const kubectl = await cluster.kubeCtl.getPath();
|
||||
const kubectl = await cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectlPath}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
||||
|
||||
return JSON.parse(stdout).items;
|
||||
} catch {
|
||||
|
||||
+6
-3
@@ -146,9 +146,12 @@ app.on("ready", async () => {
|
||||
filesystemStore.load(),
|
||||
]);
|
||||
|
||||
const lensProxy = LensProxy.createInstance(handleWsUpgrade);
|
||||
|
||||
ClusterManager.createInstance();
|
||||
const lensProxy = LensProxy.createInstance(
|
||||
handleWsUpgrade,
|
||||
req => ClusterManager.getInstance().getClusterForRequest(req),
|
||||
);
|
||||
|
||||
ClusterManager.createInstance().init();
|
||||
KubeconfigSyncManager.createInstance();
|
||||
|
||||
try {
|
||||
|
||||
@@ -22,11 +22,15 @@
|
||||
import type { IpcMainInvokeEvent } from "electron";
|
||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../common/cluster-ipc";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
||||
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { ipcMainHandle } from "../../common/ipc";
|
||||
import { catalogEntityRegistry } from "../catalog";
|
||||
import { ClusterManager } from "../cluster-manager";
|
||||
import { bundledKubectlPath } from "../kubectl";
|
||||
import logger from "../logger";
|
||||
import { promiseExecFile } from "../promise-exec";
|
||||
import { ResourceApplier } from "../resource-applier";
|
||||
|
||||
export function initIpcMainHandlers() {
|
||||
@@ -73,6 +77,29 @@ export function initIpcMainHandlers() {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
|
||||
appEventBus.emit({ name: "cluster", action: "remove" });
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClusterManager.getInstance().deleting.add(clusterId);
|
||||
cluster.disconnect();
|
||||
clusterFrameMap.delete(cluster.id);
|
||||
const kubectlPath = bundledKubectlPath();
|
||||
const args = ["config", "delete-context", cluster.contextName, "--kubeconfig", cluster.kubeConfigPath];
|
||||
|
||||
try {
|
||||
await promiseExecFile(kubectlPath, args);
|
||||
} catch ({ stderr }) {
|
||||
logger.error(`[CLUSTER-REMOVE]: failed to remove cluster: ${stderr}`, { clusterId, context: cluster.contextName });
|
||||
|
||||
throw `Failed to remove cluster: ${stderr}`;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
|
||||
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
+12
-6
@@ -31,6 +31,7 @@ import { UserStore } from "../common/user-store";
|
||||
import { customRequest } from "../common/request";
|
||||
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
||||
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
const bundledVersion = getBundledKubectlVersion();
|
||||
const kubectlMap: Map<string, string> = new Map([
|
||||
@@ -92,14 +93,19 @@ export class Kubectl {
|
||||
|
||||
// Returns the single bundled Kubectl instance
|
||||
public static bundled() {
|
||||
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
|
||||
|
||||
return Kubectl.bundledInstance;
|
||||
return Kubectl.bundledInstance ??= new Kubectl(Kubectl.bundledKubectlVersion);
|
||||
}
|
||||
|
||||
constructor(clusterVersion: string) {
|
||||
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
|
||||
const minorVersion = versionParts[1];
|
||||
let version: SemVer;
|
||||
|
||||
try {
|
||||
version = new SemVer(clusterVersion, { includePrerelease: false });
|
||||
} catch {
|
||||
version = new SemVer(Kubectl.bundledKubectlVersion);
|
||||
}
|
||||
|
||||
const minorVersion = `${version.major}.${version.minor}`;
|
||||
|
||||
/* minorVersion is the first two digits of kube server version
|
||||
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
|
||||
@@ -107,7 +113,7 @@ export class Kubectl {
|
||||
this.kubectlVersion = kubectlMap.get(minorVersion);
|
||||
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
|
||||
} else {
|
||||
this.kubectlVersion = versionParts[1] + versionParts[2];
|
||||
this.kubectlVersion = version.format();
|
||||
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import * as util from "util";
|
||||
import { exec } from "child_process";
|
||||
import { exec, execFile } from "child_process";
|
||||
|
||||
export const promiseExec = util.promisify(exec);
|
||||
export const promiseExecFile = util.promisify(execFile);
|
||||
@@ -29,7 +29,7 @@ import { Router } from "../router";
|
||||
import type { ContextHandler } from "../context-handler";
|
||||
import logger from "../logger";
|
||||
import { Singleton } from "../../common/utils";
|
||||
import { ClusterManager } from "../cluster-manager";
|
||||
import type { Cluster } from "../cluster";
|
||||
|
||||
type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
|
||||
|
||||
@@ -42,7 +42,7 @@ export class LensProxy extends Singleton {
|
||||
|
||||
public port: number;
|
||||
|
||||
constructor(handleWsUpgrade: WSUpgradeHandler) {
|
||||
constructor(handleWsUpgrade: WSUpgradeHandler, protected getClusterForRequest: (req: http.IncomingMessage) => Cluster | undefined) {
|
||||
super();
|
||||
|
||||
const proxy = this.createProxy();
|
||||
@@ -104,7 +104,7 @@ export class LensProxy extends Singleton {
|
||||
}
|
||||
|
||||
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
const cluster = this.getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||
@@ -220,7 +220,7 @@ export class LensProxy extends Singleton {
|
||||
}
|
||||
|
||||
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
const cluster = this.getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||
|
||||
@@ -36,15 +36,15 @@ export class ResourceApplier {
|
||||
|
||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||
resource = this.sanitizeObject(resource);
|
||||
appEventBus.emit({name: "resource", action: "apply"});
|
||||
appEventBus.emit({ name: "resource", action: "apply" });
|
||||
|
||||
return await this.kubectlApply(yaml.safeDump(resource));
|
||||
}
|
||||
|
||||
protected async kubectlApply(content: string): Promise<string> {
|
||||
const { kubeCtl } = this.cluster;
|
||||
const kubectlPath = await kubeCtl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
const kubectl = await this.cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const fileName = tempy.file({ name: "resource.yaml" });
|
||||
@@ -82,9 +82,9 @@ export class ResourceApplier {
|
||||
}
|
||||
|
||||
protected async kubectlCmdAll(subCmd: string, resources: string[], args: string[] = []): Promise<string> {
|
||||
const { kubeCtl } = this.cluster;
|
||||
const kubectlPath = await kubeCtl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
const kubectl = await this.cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tmpDir = tempy.directory();
|
||||
|
||||
Reference in new issue
Block a user