1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

remove ClusterFeature

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-05-10 09:29:10 +03:00
parent f550018f39
commit f47e2a8477
10 changed files with 207 additions and 224 deletions

View File

@ -2,3 +2,5 @@ apiVersion: v1
kind: Namespace
metadata:
name: lens-metrics
labels:
app: lens-metrics

View File

@ -1,4 +1,4 @@
import { ClusterFeature, Catalog, K8sApi } from "@k8slens/extensions";
import { Catalog, K8sApi } from "@k8slens/extensions";
import semver from "semver";
import * as path from "path";
@ -27,86 +27,67 @@ export interface MetricsConfiguration {
storageClass: string;
}
export class MetricsFeature extends ClusterFeature.Feature {
export interface MetricsStatus {
installed: boolean;
canUpgrade: boolean;
}
export class MetricsFeature {
name = "metrics";
latestVersion = "v2.19.3-lens1";
templateContext: MetricsConfiguration = {
prometheus: {
enabled: false
},
persistence: {
enabled: false,
storageClass: null,
size: "20G",
},
nodeExporter: {
enabled: false,
},
retention: {
time: "2d",
size: "5GB",
},
kubeStateMetrics: {
enabled: false,
},
alertManagers: null,
replicas: 1,
storageClass: null,
};
protected stack: K8sApi.ResourceStack;
async install(cluster: Catalog.KubernetesCluster): Promise<void> {
constructor(protected cluster: Catalog.KubernetesCluster) {
this.stack = new K8sApi.ResourceStack(cluster);
}
get resourceFolder() {
return path.join(__dirname, "../resources/");
}
async install(config: MetricsConfiguration): Promise<string> {
// Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
const storageClassApi = K8sApi.forCluster(this.cluster, K8sApi.StorageClass);
const scs = await storageClassApi.list();
this.templateContext.persistence.enabled = scs.some(sc => (
config.persistence.enabled = scs.some(sc => (
sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" ||
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
));
await super.applyResources(cluster, path.join(__dirname, "../resources/"));
return this.stack.kubectlApplyFolder(this.resourceFolder, config);
}
async upgrade(cluster: Catalog.KubernetesCluster): Promise<void> {
return this.install(cluster);
async upgrade(config: MetricsConfiguration): Promise<string> {
return this.install(config);
}
async updateStatus(cluster: Catalog.KubernetesCluster): Promise<ClusterFeature.FeatureStatus> {
async getStatus(): Promise<MetricsStatus> {
const status: MetricsStatus = { installed: false, canUpgrade: false};
try {
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
const statefulSet = K8sApi.forCluster(this.cluster, K8sApi.StatefulSet);
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
if (prometheus?.kind) {
this.status.installed = true;
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true);
const currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
const pvcTemplate = prometheus.spec.volumeClaimTemplates[0];
if (pvcTemplate) {
this.templateContext.persistence.enabled = true;
this.templateContext.persistence.size = pvcTemplate.spec.resources.requests.storage;
}
status.installed = true;
status.canUpgrade = semver.lt(currentVersion, this.latestVersion, true);
} else {
this.status.installed = false;
status.installed = false;
}
} catch(e) {
if (e?.error?.code === 404) {
this.status.installed = false;
status.installed = false;
}
}
return this.status;
return status;
}
async uninstall(cluster: Catalog.KubernetesCluster): Promise<void> {
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
await namespaceApi.delete({name: "lens-metrics"});
await clusterRoleBindingApi.delete({name: "lens-prometheus"});
await clusterRoleApi.delete({name: "lens-prometheus"});
async uninstall(config: MetricsConfiguration): Promise<string> {
return this.stack.kubectlDeleteFolder(this.resourceFolder, config);
}
}

View File

@ -2,7 +2,8 @@ import React from "react";
import { Component, Catalog, K8sApi } from "@k8slens/extensions";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { MetricsFeature } from "./metrics-feature";
import { MetricsFeature, MetricsConfiguration } from "./metrics-feature";
interface Props {
cluster: Catalog.KubernetesCluster;
}
@ -17,28 +18,42 @@ export class MetricsSettings extends React.Component<Props> {
@observable canUpgrade = false;
@observable upgrading = false;
pvcOptions = [
{ value: "", label: "Use EmptyDir (no PVC)" },
{ value: "5GB", label: "5GB" },
{ value: "10GB", label: "10GB" },
{ value: "20GB", label: "20GB" },
{ value: "40GB", label: "40GB" },
{ value: "80GB", label: "80GB" },
];
featureSet: MetricsFeature;
config: MetricsConfiguration = {
prometheus: {
enabled: false
},
persistence: {
enabled: false,
storageClass: null,
size: "20G",
},
nodeExporter: {
enabled: false,
},
retention: {
time: "2d",
size: "5GB",
},
kubeStateMetrics: {
enabled: false,
},
alertManagers: null,
replicas: 1,
storageClass: null,
};
feature: MetricsFeature;
async componentDidMount() {
this.featureSet = new MetricsFeature();
this.feature = new MetricsFeature(this.props.cluster);
await this.updateFeatureStates();
}
async updateFeatureStates() {
await this.featureSet.updateStatus(this.props.cluster);
this.canUpgrade = this.featureSet.status.canUpgrade;
const status = await this.feature.getStatus();
this.featureStates.prometheus = this.featureSet.status.installed;
this.canUpgrade = status.canUpgrade;
this.featureStates.prometheus = status.installed;
const deployment = K8sApi.forCluster(this.props.cluster, K8sApi.Deployment);
@ -68,24 +83,22 @@ export class MetricsSettings extends React.Component<Props> {
}
async save() {
const ctx = this.featureSet.templateContext;
this.config.prometheus.enabled = !!this.featureStates.prometheus;
this.config.kubeStateMetrics.enabled = !!this.featureStates.kubeStateMetrics;
this.config.nodeExporter.enabled = !!this.featureStates.nodeExporter;
ctx.prometheus.enabled = !!this.featureStates.prometheus;
ctx.kubeStateMetrics.enabled = !!this.featureStates.kubeStateMetrics;
ctx.nodeExporter.enabled = !!this.featureStates.nodeExporter;
console.log(this.config);
if (!ctx.prometheus.enabled && !ctx.kubeStateMetrics.enabled && !ctx.nodeExporter.enabled) {
await this.featureSet.uninstall(this.props.cluster);
if (!this.config.prometheus.enabled && !this.config.kubeStateMetrics.enabled && !this.config.nodeExporter.enabled) {
await this.feature.uninstall(this.config);
} else {
await this.featureSet.install(this.props.cluster);
await this.feature.install(this.config);
}
}
async togglePrometheus(enabled: boolean) {
this.featureStates.prometheus = enabled;
this.featureSet.templateContext.prometheus.enabled = this.featureStates.prometheus;
try {
await this.save();
} catch(error) {
@ -97,8 +110,6 @@ export class MetricsSettings extends React.Component<Props> {
async toggleKubeStateMetrics(enabled: boolean) {
this.featureStates.kubeStateMetrics = enabled;
this.featureSet.templateContext.kubeStateMetrics.enabled = this.featureStates.kubeStateMetrics;
try {
await this.save();
} catch(error) {
@ -110,8 +121,6 @@ export class MetricsSettings extends React.Component<Props> {
async toggleNodeExporter(enabled: boolean) {
this.featureStates.nodeExporter = enabled;
this.featureSet.templateContext.nodeExporter.enabled = this.featureStates.nodeExporter;
try {
await this.save();
} catch(error) {

View File

@ -10,6 +10,7 @@ export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
if (ipcMain) {
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
@ -64,4 +65,23 @@ if (ipcMain) {
throw `${clusterId} is not a valid cluster id`;
}
});
handleRequest(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-delete-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlApplyAll(resources);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
}

View File

@ -0,0 +1,102 @@
import fs from "fs";
import path from "path";
import hb from "handlebars";
import { ResourceApplier } from "../../main/resource-applier";
import { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger";
import { app } from "electron";
import { requestMain } from "../ipc";
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
export class ResourceStack {
/**
* this field sets the template parameters that are to be applied to any templated kubernetes resources that are to be installed.
* See the renderTemplates() method for more details
*/
templateContext: any;
constructor(protected cluster: KubernetesCluster) {}
/**
*
* @param folderPath folder path that is searched for files defining kubernetes resources.
* @param templateContext sets the template parameters that are to be applied to any templated kubernetes resources that are to be applied.
*/
async kubectlApplyFolder(folderPath: string, templateContext?: any): Promise<string> {
const resources = this.renderTemplates(folderPath, templateContext);
return this.applyResources(resources);
}
/**
*
* @param folderPath folder path that is searched for files defining kubernetes resources.
* @param templateContext sets the template parameters that are to be applied to any templated kubernetes resources that are to be applied.
*/
async kubectlDeleteFolder(folderPath: string, templateContext?: any): Promise<string> {
const resources = this.renderTemplates(folderPath, templateContext);
return this.deleteResources(resources);
}
protected async applyResources(resources: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);
}
if (app) {
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources);
} else {
const response = await requestMain(clusterKubectlApplyAllHandler, this.cluster.metadata.uid, resources);
if (response.stderr) {
throw new Error(response.stderr);
}
return response.stdout;
}
}
protected async deleteResources(resources: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);
}
if (app) {
return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources);
} else {
const response = await requestMain(clusterKubectlDeleteAllHandler, this.cluster.metadata.uid, resources);
if (response.stderr) {
throw new Error(response.stderr);
}
return response.stdout;
}
}
protected renderTemplates(folderPath: string, templateContext: any): string[] {
const resources: string[] = [];
logger.info(`[RESOURCE-STACK]: 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(templateContext));
} else {
resources.push(raw.toString());
}
});
return resources;
}
}

View File

@ -1,135 +0,0 @@
import fs from "fs";
import path from "path";
import hb from "handlebars";
import { observable } from "mobx";
import { ResourceApplier } from "../main/resource-applier";
import { KubernetesCluster } from "./core-api/catalog";
import logger from "../main/logger";
import { app } from "electron";
import { requestMain } from "../common/ipc";
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
import { ClusterStore } from "../common/cluster-store";
export interface ClusterFeatureStatus {
/** feature's current version, as set by the implementation */
currentVersion: string;
/** feature's latest version, as set by the implementation */
latestVersion: string;
/** whether the feature is installed or not, as set by the implementation */
installed: boolean;
/** whether the feature can be upgraded or not, as set by the implementation */
canUpgrade: boolean;
}
export abstract class ClusterFeature {
/**
* this field sets the template parameters that are to be applied to any templated kubernetes resources that are to be installed for the feature.
* See the renderTemplates() method for more details
*/
templateContext: any;
/**
* this field holds the current feature status, is accessed directly by Lens
*/
@observable status: ClusterFeatureStatus = {
currentVersion: null,
installed: false,
latestVersion: null,
canUpgrade: false
};
/**
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be installed. The implementation
* of this method should install kubernetes resources using the applyResources() method, or by directly accessing the kubernetes api (K8sApi)
*
* @param cluster the cluster that the feature is to be installed on
*/
abstract install(cluster: KubernetesCluster): Promise<void>;
/**
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
* of this method should upgrade the kubernetes resources already installed, if relevant to the feature
*
* @param cluster the cluster that the feature is to be upgraded on
*/
abstract upgrade(cluster: KubernetesCluster): Promise<void>;
/**
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
* of this method should uninstall kubernetes resources using the kubernetes api (K8sApi)
*
* @param cluster the cluster that the feature is to be uninstalled from
*/
abstract uninstall(cluster: KubernetesCluster): Promise<void>;
/**
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
* of this method should provide the current status information. The currentVersion and latestVersion fields may be displayed by Lens in describing the feature.
* The installed field should be set to true if the feature has been installed, otherwise false. Also, Lens relies on the canUpgrade field to determine if the feature
* can be upgraded so the implementation should set the canUpgrade field according to specific rules for the feature, if relevant.
*
* @param cluster the cluster that the feature may be installed on
*
* @return a promise, resolved with the updated ClusterFeatureStatus
*/
abstract updateStatus(cluster: KubernetesCluster): Promise<ClusterFeatureStatus>;
/**
* this is a helper method that conveniently applies kubernetes resources to the cluster.
*
* @param cluster the cluster that the resources are to be applied to
* @param resourceSpec as a string type this is a folder path that is searched for files specifying kubernetes resources. The files are read and if any of the resource
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
* cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster
*/
protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
let resources: string[];
const clusterModel = ClusterStore.getInstance().getById(cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);
}
if ( typeof resourceSpec === "string" ) {
resources = this.renderTemplates(resourceSpec);
} else {
resources = resourceSpec;
}
if (app) {
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources);
} else {
return await requestMain(clusterKubectlApplyAllHandler, cluster.metadata.uid, resources);
}
}
/**
* this is a helper method that conveniently reads kubernetes resource files into a string array. It also fills templated resource files with the template parameter values
* specified by the templateContext field. Templated files must end with the extension '.hb' and the template syntax must be compatible with handlebars.js
*
* @param folderPath this is a folder path that is searched for files defining kubernetes resources.
*
* @return an array of strings, each string being the contents of a resource file found in the folder path. This can be passed directly to applyResources()
*/
protected renderTemplates(folderPath: string): string[] {
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.templateContext));
} else {
resources.push(raw.toString());
}
});
return resources;
}
}

View File

@ -1,2 +0,0 @@
export { ClusterFeature as Feature } from "../cluster-feature";
export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature";

View File

@ -7,7 +7,6 @@ import * as App from "./app";
import * as EventBus from "./event-bus";
import * as Store from "./stores";
import * as Util from "./utils";
import * as ClusterFeature from "./cluster-feature";
import * as Interface from "../interfaces";
import * as Catalog from "./catalog";
@ -15,7 +14,6 @@ export {
App,
EventBus,
Catalog,
ClusterFeature,
Interface,
Store,
Util,

View File

@ -1,4 +1,5 @@
export { isAllowedResource } from "../../common/rbac";
export { ResourceStack } from "../../common/k8s/resource-stack";
export { apiManager } from "../../renderer/api/api-manager";
export { KubeObjectStore } from "../../renderer/kube-object.store";
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";

View File

@ -53,6 +53,14 @@ export class ResourceApplier {
}
public async kubectlApplyAll(resources: string[]): Promise<string> {
return this.kubectlCmdAll("apply", resources);
}
public async kubectlDeleteAll(resources: string[]): Promise<string> {
return this.kubectlCmdAll("delete", resources);
}
protected async kubectlCmdAll(subCmd: string, resources: string[]): Promise<string> {
const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
@ -64,17 +72,16 @@ export class ResourceApplier {
resources.forEach((resource, index) => {
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
});
const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${tmpDir}"`;
const cmd = `"${kubectlPath}" ${subCmd} --kubeconfig "${proxyKubeconfigPath}" -o json -f "${tmpDir}"`;
console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout) => {
if (error) {
const splittedError = error.toString().split(`.yaml": `);
if (splittedError[1]) {
reject(`Error applying manifests: ${splittedError[1]}`);
reject(splittedError[1]);
} else {
reject(`Error applying manifests:${error}`);
reject(error);
}
return;