From f47e2a8477c680c3c38d38d329c2b02df38e98f2 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 10 May 2021 09:29:10 +0300 Subject: [PATCH] remove ClusterFeature Signed-off-by: Jari Kolehmainen --- .../resources/01-namespace.yml | 2 + .../src/metrics-feature.ts | 87 +++++------ .../src/metrics-settings.tsx | 65 +++++---- src/common/cluster-ipc.ts | 20 +++ src/common/k8s/resource-stack.ts | 102 +++++++++++++ src/extensions/cluster-feature.ts | 135 ------------------ src/extensions/core-api/cluster-feature.ts | 2 - src/extensions/core-api/index.ts | 2 - src/extensions/renderer-api/k8s-api.ts | 1 + src/main/resource-applier.ts | 15 +- 10 files changed, 207 insertions(+), 224 deletions(-) create mode 100644 src/common/k8s/resource-stack.ts delete mode 100644 src/extensions/cluster-feature.ts delete mode 100644 src/extensions/core-api/cluster-feature.ts diff --git a/extensions/metrics-cluster-feature/resources/01-namespace.yml b/extensions/metrics-cluster-feature/resources/01-namespace.yml index 85d13c1046..96a5e074d9 100644 --- a/extensions/metrics-cluster-feature/resources/01-namespace.yml +++ b/extensions/metrics-cluster-feature/resources/01-namespace.yml @@ -2,3 +2,5 @@ apiVersion: v1 kind: Namespace metadata: name: lens-metrics + labels: + app: lens-metrics diff --git a/extensions/metrics-cluster-feature/src/metrics-feature.ts b/extensions/metrics-cluster-feature/src/metrics-feature.ts index 11181f9199..2eddf04934 100644 --- a/extensions/metrics-cluster-feature/src/metrics-feature.ts +++ b/extensions/metrics-cluster-feature/src/metrics-feature.ts @@ -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 { + constructor(protected cluster: Catalog.KubernetesCluster) { + this.stack = new K8sApi.ResourceStack(cluster); + } + + get resourceFolder() { + return path.join(__dirname, "../resources/"); + } + + async install(config: MetricsConfiguration): Promise { // 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 { - return this.install(cluster); + async upgrade(config: MetricsConfiguration): Promise { + return this.install(config); } - async updateStatus(cluster: Catalog.KubernetesCluster): Promise { + async getStatus(): Promise { + 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 { - 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 { + return this.stack.kubectlDeleteFolder(this.resourceFolder, config); } } diff --git a/extensions/metrics-cluster-feature/src/metrics-settings.tsx b/extensions/metrics-cluster-feature/src/metrics-settings.tsx index 1473c27c37..af94782b6e 100644 --- a/extensions/metrics-cluster-feature/src/metrics-settings.tsx +++ b/extensions/metrics-cluster-feature/src/metrics-settings.tsx @@ -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 { @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 { } 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 { 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 { async toggleNodeExporter(enabled: boolean) { this.featureStates.nodeExporter = enabled; - this.featureSet.templateContext.nodeExporter.enabled = this.featureStates.nodeExporter; - try { await this.save(); } catch(error) { diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index b0165dc74f..f543dc34f7 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -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`; + } + }); } diff --git a/src/common/k8s/resource-stack.ts b/src/common/k8s/resource-stack.ts new file mode 100644 index 0000000000..e3f9d075e3 --- /dev/null +++ b/src/common/k8s/resource-stack.ts @@ -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 { + 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 { + const resources = this.renderTemplates(folderPath, templateContext); + + return this.deleteResources(resources); + } + + protected async applyResources(resources: string[]): Promise { + 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 { + 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; + } +} diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts deleted file mode 100644 index e762ae5222..0000000000 --- a/src/extensions/cluster-feature.ts +++ /dev/null @@ -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; - - /** - * 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; - - /** - * 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; - - /** - * 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; - - /** - * 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; - } -} diff --git a/src/extensions/core-api/cluster-feature.ts b/src/extensions/core-api/cluster-feature.ts deleted file mode 100644 index 170f4543f3..0000000000 --- a/src/extensions/core-api/cluster-feature.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ClusterFeature as Feature } from "../cluster-feature"; -export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"; diff --git a/src/extensions/core-api/index.ts b/src/extensions/core-api/index.ts index f0989fc5bd..b111387b86 100644 --- a/src/extensions/core-api/index.ts +++ b/src/extensions/core-api/index.ts @@ -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, diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts index 071d8365ab..b0d5183548 100644 --- a/src/extensions/renderer-api/k8s-api.ts +++ b/src/extensions/renderer-api/k8s-api.ts @@ -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"; diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index cb245c4788..963b906689 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -53,6 +53,14 @@ export class ResourceApplier { } public async kubectlApplyAll(resources: string[]): Promise { + return this.kubectlCmdAll("apply", resources); + } + + public async kubectlDeleteAll(resources: string[]): Promise { + return this.kubectlCmdAll("delete", resources); + } + + protected async kubectlCmdAll(subCmd: string, resources: string[]): Promise { 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;