1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/common/cluster-store.ts
Sebastian Malton 6bb553a5da Cleanup Cluster, ClusterManger, et al.
- Removed `getFreePort` as its use is always a race condition. Change
  all uses of it to retrive the port after listening

- Added `getPortFrom` as a helper function to read a port from a stream

- Remove `Cluster.ownerRef` as it is outdated and no longer needed for 5.0

- Remove `Cluster.enabled`, no longer needed because of above

- Removed `Cluster.init`, moved its contents into `Cluster.constructor`
  as nothing in that function is asyncronous. Currently only being run
  on `main` as a stop gap until `renderer` gets its own version of
  `Cluster`

- Refactored `LensProxy` so as to prevent `pty.node` (a NodeJS
  extension) being included in `webpack.extension.ts`'s run

- Removed the passing around of the proxy port as that can now be
  accessed from an instance of `LensProxy`

Signed-off-by: Sebastian Malton <sebastian@malton.name>
2021-05-03 16:52:15 -04:00

351 lines
8.9 KiB
TypeScript

import path from "path";
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra";
import { action, comparer, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store";
import logger from "../main/logger";
import { appEventBus } from "./event-bus";
import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
import { disposer, noop } from "./utils";
export interface ClusterIconUpload {
clusterId: string;
name: string;
path: string;
}
export interface ClusterMetadata {
[key: string]: string | number | boolean | object;
}
export type ClusterPrometheusMetadata = {
success?: boolean;
provider?: string;
autoDetected?: boolean;
};
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[];
}
export type ClusterId = string;
export interface UpdateClusterModel extends Omit<ClusterModel, "id"> {
id?: ClusterId;
}
export interface ClusterModel {
/** Unique id for a cluster */
id: ClusterId;
/** Path to cluster kubeconfig */
kubeConfigPath: string;
/**
* Workspace id
*
* @deprecated
*/
workspace?: string;
/** User context in kubeconfig */
contextName?: string;
/** Preferences */
preferences?: ClusterPreferences;
/** Metadata */
metadata?: ClusterMetadata;
/** List of accessible namespaces */
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
}
export interface ClusterPreferences extends ClusterPrometheusPreferences {
terminalCWD?: string;
clusterName?: string;
iconOrder?: number;
icon?: string;
httpsProxy?: string;
hiddenMetrics?: string[];
}
export interface ClusterPrometheusPreferences {
prometheus?: {
namespace: string;
service: string;
port: number;
prefix: string;
};
prometheusProvider?: {
type: string;
};
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
static get storedKubeConfigFolder(): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
}
static getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
}
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
return filePath;
}
@observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
private static stateRequestChannel = "cluster:states";
protected disposer = disposer();
constructor() {
super({
configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
migrations,
});
this.pushStateToViewsAutomatically();
}
async load() {
await super.load();
type clusterStateSync = {
id: string;
state: ClusterState;
};
if (ipcRenderer) {
logger.info("[CLUSTER-STORE] requesting initial state sync");
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
clusterStates.forEach((clusterState) => {
const cluster = this.getById(clusterState.id);
if (cluster) {
cluster.setState(clusterState.state);
}
});
} else if (ipcMain) {
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
const states: clusterStateSync[] = [];
this.clustersList.forEach((cluster) => {
states.push({
state: cluster.getState(),
id: cluster.id
});
});
return states;
});
}
}
protected pushStateToViewsAutomatically() {
if (ipcMain) {
this.disposer.push(
reaction(() => this.connectedClustersList, () => {
this.pushState();
}),
() => unsubscribeAllFromBroadcast("cluster:state"),
);
}
}
registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`);
subscribeToBroadcast("cluster:state", (event, clusterId: string, state: ClusterState) => {
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
this.getById(clusterId)?.setState(state);
});
}
unregisterIpcListener() {
super.unregisterIpcListener();
this.disposer();
}
pushState() {
this.clusters.forEach((c) => {
c.pushState();
});
}
get activeClusterId() {
return this.activeCluster;
}
@computed get clustersList(): Cluster[] {
return Array.from(this.clusters.values());
}
@computed get active(): Cluster | null {
return this.getById(this.activeCluster);
}
@computed get connectedClustersList(): Cluster[] {
return this.clustersList.filter((c) => !c.disconnected);
}
isActive(id: ClusterId) {
return this.activeCluster === id;
}
isMetricHidden(resource: ResourceType) {
return Boolean(this.active?.preferences.hiddenMetrics?.includes(resource));
}
@action
setActive(clusterId: ClusterId) {
this.activeCluster = this.clusters.has(clusterId)
? clusterId
: null;
}
deactivate(id: ClusterId) {
if (this.isActive(id)) {
this.setActive(null);
}
}
hasClusters() {
return this.clusters.size > 0;
}
getById(id: ClusterId): Cluster | null {
return this.clusters.get(id) ?? null;
}
@action
addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = [];
models.forEach(model => {
clusters.push(this.addCluster(model));
});
return clusters;
}
@action
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" });
const cluster = clusterOrModel instanceof Cluster
? clusterOrModel
: new Cluster(clusterOrModel);
this.clusters.set(cluster.id, cluster);
return cluster;
}
async removeCluster(model: ClusterModel) {
await this.removeById(model.id);
}
@action
async removeById(clusterId: ClusterId) {
appEventBus.emit({ name: "cluster", action: "remove" });
const cluster = this.getById(clusterId);
if (cluster) {
this.clusters.delete(clusterId);
if (this.activeCluster === clusterId) {
this.setActive(null);
}
// remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
await unlink(cluster.kubeConfigPath).catch(noop);
}
}
}
@action
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = this.clusters.toJS();
const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>();
// update new clusters
for (const clusterModel of clusters) {
try {
let cluster = currentClusters.get(clusterModel.id);
if (cluster) {
cluster.updateModel(clusterModel);
} else {
cluster = new Cluster(clusterModel);
}
newClusters.set(clusterModel.id, cluster);
} catch {
// ignore
}
}
// update removed clusters
currentClusters.forEach(cluster => {
if (!newClusters.has(cluster.id)) {
removedClusters.set(cluster.id, cluster);
}
});
this.setActive(activeCluster);
this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters);
}
toJSON(): ClusterStoreModel {
return toJS({
activeCluster: this.activeCluster,
clusters: this.clustersList.map(cluster => cluster.toJSON()),
}, {
recurseEverything: true
});
}
}
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
return subDomains.slice(-2, -1)[0]; // ClusterId or undefined
}
export function getClusterFrameUrl(clusterId: ClusterId) {
return `//${clusterId}.${location.host}`;
}
export function getHostedClusterId() {
return getClusterIdFromHost(location.host);
}
export function getHostedCluster(): Cluster {
return ClusterStore.getInstance().getById(getHostedClusterId());
}