mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Allow extensions to define cluster features (#1125)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
f3a0059355
commit
62ae7771df
81 files changed
+4069
-510
No files matched your search
@@ -49,6 +49,8 @@ export class ClusterManager {
|
||||
// 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())
|
||||
} else {
|
||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||
cluster = clusterStore.getById(clusterId)
|
||||
|
||||
+1
-23
@@ -1,7 +1,6 @@
|
||||
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 { FeatureStatusMap } from "./feature"
|
||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { broadcastIpc } from "../common/ipc";
|
||||
@@ -10,7 +9,6 @@ import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
||||
import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers"
|
||||
import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
|
||||
import request, { RequestPromiseOptions } from "request-promise-native"
|
||||
import { apiResources } from "../common/rbac";
|
||||
import logger from "./logger"
|
||||
@@ -47,7 +45,6 @@ export interface ClusterState extends ClusterModel {
|
||||
isAdmin: boolean;
|
||||
allowedNamespaces: string[]
|
||||
allowedResources: string[]
|
||||
features: FeatureStatusMap;
|
||||
}
|
||||
|
||||
export class Cluster implements ClusterModel {
|
||||
@@ -78,7 +75,6 @@ export class Cluster implements ClusterModel {
|
||||
@observable eventCount = 0;
|
||||
@observable preferences: ClusterPreferences = {};
|
||||
@observable metadata: ClusterMetadata = {};
|
||||
@observable features: FeatureStatusMap = {};
|
||||
@observable allowedNamespaces: string[] = [];
|
||||
@observable allowedResources: string[] = [];
|
||||
|
||||
@@ -194,12 +190,7 @@ export class Cluster implements ClusterModel {
|
||||
await this.whenInitialized;
|
||||
await this.refreshConnectionStatus();
|
||||
if (this.accessible) {
|
||||
const [features, isAdmin] = await Promise.all([
|
||||
getFeatures(this),
|
||||
this.isClusterAdmin(),
|
||||
]);
|
||||
this.features = features;
|
||||
this.isAdmin = isAdmin;
|
||||
this.isAdmin = await this.isClusterAdmin();
|
||||
await Promise.all([
|
||||
this.refreshEvents(),
|
||||
this.refreshAllowedResources(),
|
||||
@@ -250,18 +241,6 @@ export class Cluster implements ClusterModel {
|
||||
return this.kubeconfigManager.getPath()
|
||||
}
|
||||
|
||||
async installFeature(name: string, config: any) {
|
||||
return installFeature(name, this, config)
|
||||
}
|
||||
|
||||
async upgradeFeature(name: string, config: any) {
|
||||
return upgradeFeature(name, this, config)
|
||||
}
|
||||
|
||||
async uninstallFeature(name: string) {
|
||||
return uninstallFeature(name, this)
|
||||
}
|
||||
|
||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
const apiUrl = this.kubeProxyUrl + path;
|
||||
return request(apiUrl, {
|
||||
@@ -400,7 +379,6 @@ export class Cluster implements ClusterModel {
|
||||
accessible: this.accessible,
|
||||
failureReason: this.failureReason,
|
||||
isAdmin: this.isAdmin,
|
||||
features: this.features,
|
||||
eventCount: this.eventCount,
|
||||
allowedNamespaces: this.allowedNamespaces,
|
||||
allowedResources: this.allowedResources,
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { KubeConfig } from "@kubernetes/client-node"
|
||||
import logger from "./logger";
|
||||
import { Cluster } from "./cluster";
|
||||
import { Feature, FeatureStatusMap, FeatureMap } from "./feature"
|
||||
import { MetricsFeature } from "../features/metrics"
|
||||
import { UserModeFeature } from "../features/user-mode"
|
||||
|
||||
const ALL_FEATURES: Map<string, Feature> = new Map([
|
||||
[MetricsFeature.id, new MetricsFeature(null)],
|
||||
[UserModeFeature.id, new UserModeFeature(null)],
|
||||
]);
|
||||
|
||||
export async function getFeatures(cluster: Cluster): Promise<FeatureStatusMap> {
|
||||
const result: FeatureStatusMap = {};
|
||||
logger.debug(`features for ${cluster.contextName}`);
|
||||
|
||||
for (const [key, feature] of ALL_FEATURES) {
|
||||
logger.debug(`feature ${key}`);
|
||||
logger.debug("getting feature status...");
|
||||
|
||||
const kc = new KubeConfig();
|
||||
kc.loadFromFile(cluster.getProxyKubeconfigPath());
|
||||
|
||||
result[feature.name] = await feature.featureStatus(kc);
|
||||
}
|
||||
|
||||
logger.debug(`getFeatures resolving with features: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export async function installFeature(name: string, cluster: Cluster, config: any): Promise<void> {
|
||||
// TODO Figure out how to handle config stuff
|
||||
return ALL_FEATURES.get(name).install(cluster)
|
||||
}
|
||||
|
||||
export async function upgradeFeature(name: string, cluster: Cluster, config: any): Promise<void> {
|
||||
// TODO Figure out how to handle config stuff
|
||||
return ALL_FEATURES.get(name).upgrade(cluster)
|
||||
}
|
||||
|
||||
export async function uninstallFeature(name: string, cluster: Cluster): Promise<void> {
|
||||
return ALL_FEATURES.get(name).uninstall(cluster)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path"
|
||||
import hb from "handlebars"
|
||||
import { ResourceApplier } from "./resource-applier"
|
||||
import { CoreV1Api, KubeConfig, Watch } from "@kubernetes/client-node"
|
||||
import { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { isDevelopment } from "../common/vars";
|
||||
|
||||
export type FeatureStatusMap = Record<string, FeatureStatus>
|
||||
export type FeatureMap = Record<string, Feature>
|
||||
|
||||
export interface FeatureInstallRequest {
|
||||
clusterId: string;
|
||||
name: string;
|
||||
config?: any;
|
||||
}
|
||||
|
||||
export interface FeatureStatus {
|
||||
currentVersion: string;
|
||||
installed: boolean;
|
||||
latestVersion: string;
|
||||
canUpgrade: boolean;
|
||||
}
|
||||
|
||||
export abstract class Feature {
|
||||
public name: string;
|
||||
public latestVersion: string;
|
||||
|
||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async featureStatus(kc: KubeConfig): Promise<FeatureStatus>;
|
||||
|
||||
constructor(public config: any) {
|
||||
}
|
||||
|
||||
get folderPath() {
|
||||
if (isDevelopment) {
|
||||
return path.resolve(__static, "../src/features", this.name);
|
||||
}
|
||||
return path.resolve(__static, "../features", this.name);
|
||||
}
|
||||
|
||||
async install(cluster: Cluster): Promise<void> {
|
||||
const resources = this.renderTemplates();
|
||||
try {
|
||||
await new ResourceApplier(cluster).kubectlApplyAll(resources);
|
||||
} catch (err) {
|
||||
logger.error("Installing feature error", { err, cluster });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
protected async deleteNamespace(kc: KubeConfig, name: string) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const client = kc.makeApiClient(CoreV1Api)
|
||||
const result = await client.deleteNamespace("lens-metrics", 'false', undefined, undefined, undefined, "Foreground");
|
||||
const nsVersion = result.body.metadata.resourceVersion;
|
||||
const nsWatch = new Watch(kc);
|
||||
const query: Record<string, string> = {
|
||||
resourceVersion: nsVersion,
|
||||
fieldSelector: "metadata.name=lens-metrics",
|
||||
}
|
||||
const req = await nsWatch.watch('/api/v1/namespaces', query,
|
||||
(phase, obj) => {
|
||||
if (phase === 'DELETED') {
|
||||
logger.debug(`namespace ${name} finally gone`)
|
||||
req.abort();
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
(err?: any) => {
|
||||
if (err) reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected renderTemplates(): string[] {
|
||||
const folderPath = this.folderPath;
|
||||
const resources: string[] = [];
|
||||
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
||||
fs.readdirSync(folderPath).forEach(filename => {
|
||||
const file = path.join(folderPath, filename);
|
||||
const raw = fs.readFileSync(file);
|
||||
if (filename.endsWith('.hb')) {
|
||||
const template = hb.compile(raw.toString());
|
||||
resources.push(template(this.config));
|
||||
} else {
|
||||
resources.push(raw.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user