mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Show lens-metrics on cluster settings (#2714)
* show lens-metrics on cluster settings Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ClusterFeature Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ClusterFeature Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * tweak resource applier/stack Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * update metrics stack components Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix drawer menu styles Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * clarify ui Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * built-in -> bundled Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * splitterError -> splitError Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * support multi-doc yamls Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * dedicated action button Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix headers Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * async file operations Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix bugs Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
d9c6e5c52f
commit
683e5926e0
35 files changed
+662
-443
No files matched your search
@@ -19,12 +19,15 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { ClusterFeature, Catalog, K8sApi } from "@k8slens/extensions";
|
||||
import { Catalog, K8sApi } from "@k8slens/extensions";
|
||||
import semver from "semver";
|
||||
import * as path from "path";
|
||||
|
||||
export interface MetricsConfiguration {
|
||||
// Placeholder for Metrics config structure
|
||||
prometheus: {
|
||||
enabled: boolean;
|
||||
};
|
||||
persistence: {
|
||||
enabled: boolean;
|
||||
storageClass: string;
|
||||
@@ -43,78 +46,72 @@ export interface MetricsConfiguration {
|
||||
alertManagers: string[];
|
||||
replicas: number;
|
||||
storageClass: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export class MetricsFeature extends ClusterFeature.Feature {
|
||||
name = "metrics";
|
||||
latestVersion = "v2.19.3-lens1";
|
||||
export interface MetricsStatus {
|
||||
installed: boolean;
|
||||
canUpgrade: boolean;
|
||||
}
|
||||
|
||||
templateContext: MetricsConfiguration = {
|
||||
persistence: {
|
||||
enabled: false,
|
||||
storageClass: null,
|
||||
size: "20G",
|
||||
},
|
||||
nodeExporter: {
|
||||
enabled: true,
|
||||
},
|
||||
retention: {
|
||||
time: "2d",
|
||||
size: "5GB",
|
||||
},
|
||||
kubeStateMetrics: {
|
||||
enabled: true,
|
||||
},
|
||||
alertManagers: null,
|
||||
replicas: 1,
|
||||
storageClass: null,
|
||||
};
|
||||
export class MetricsFeature {
|
||||
name = "lens-metrics";
|
||||
latestVersion = "v2.26.0-lens1";
|
||||
|
||||
async install(cluster: Catalog.KubernetesCluster): Promise<void> {
|
||||
protected stack: K8sApi.ResourceStack;
|
||||
|
||||
constructor(protected cluster: Catalog.KubernetesCluster) {
|
||||
this.stack = new K8sApi.ResourceStack(cluster, this.name);
|
||||
}
|
||||
|
||||
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"
|
||||
));
|
||||
|
||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
||||
config.version = this.latestVersion;
|
||||
|
||||
return this.stack.kubectlApplyFolder(this.resourceFolder, config, ["--prune"]);
|
||||
}
|
||||
|
||||
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 prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
||||
const namespaceApi = K8sApi.forCluster(this.cluster, K8sApi.Namespace);
|
||||
const namespace = await namespaceApi.get({name: "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);
|
||||
if (namespace?.kind) {
|
||||
const currentVersion = namespace.metadata.annotations?.extensionVersion || "0.0.0";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Component, Catalog, K8sApi } from "@k8slens/extensions";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { MetricsFeature, MetricsConfiguration } from "./metrics-feature";
|
||||
|
||||
interface Props {
|
||||
cluster: Catalog.KubernetesCluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MetricsSettings extends React.Component<Props> {
|
||||
@observable featureStates = {
|
||||
prometheus: false,
|
||||
kubeStateMetrics: false,
|
||||
nodeExporter: false
|
||||
};
|
||||
@observable canUpgrade = false;
|
||||
@observable upgrading = false;
|
||||
@observable changed = false;
|
||||
@observable inProgress = false;
|
||||
|
||||
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;
|
||||
|
||||
@computed get isTogglable() {
|
||||
if (this.inProgress) return false;
|
||||
if (!this.props.cluster.status.active) return false;
|
||||
if (this.canUpgrade) return false;
|
||||
if (!this.isActiveMetricsProvider) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get metricsProvider() {
|
||||
return this.props.cluster.spec?.metrics?.prometheus?.type || "";
|
||||
}
|
||||
|
||||
get isActiveMetricsProvider() {
|
||||
return (!this.metricsProvider || this.metricsProvider === "lens");
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.feature = new MetricsFeature(this.props.cluster);
|
||||
|
||||
await this.updateFeatureStates();
|
||||
}
|
||||
|
||||
async updateFeatureStates() {
|
||||
const status = await this.feature.getStatus();
|
||||
|
||||
this.canUpgrade = status.canUpgrade;
|
||||
|
||||
if (this.canUpgrade) {
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
const statefulSet = K8sApi.forCluster(this.props.cluster, K8sApi.StatefulSet);
|
||||
|
||||
try {
|
||||
await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
||||
this.featureStates.prometheus = true;
|
||||
} catch(e) {
|
||||
if (e?.error?.code === 404) {
|
||||
this.featureStates.prometheus = false;
|
||||
} else {
|
||||
this.featureStates.prometheus = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const deployment = K8sApi.forCluster(this.props.cluster, K8sApi.Deployment);
|
||||
|
||||
try {
|
||||
await deployment.get({name: "kube-state-metrics", namespace: "lens-metrics"});
|
||||
this.featureStates.kubeStateMetrics = true;
|
||||
} catch(e) {
|
||||
if (e?.error?.code === 404) {
|
||||
this.featureStates.kubeStateMetrics = false;
|
||||
} else {
|
||||
this.featureStates.kubeStateMetrics = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const daemonSet = K8sApi.forCluster(this.props.cluster, K8sApi.DaemonSet);
|
||||
|
||||
try {
|
||||
await daemonSet.get({name: "node-exporter", namespace: "lens-metrics"});
|
||||
this.featureStates.nodeExporter = true;
|
||||
} catch(e) {
|
||||
if (e?.error?.code === 404) {
|
||||
this.featureStates.nodeExporter = false;
|
||||
} else {
|
||||
this.featureStates.nodeExporter = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
this.config.prometheus.enabled = !!this.featureStates.prometheus;
|
||||
this.config.kubeStateMetrics.enabled = !!this.featureStates.kubeStateMetrics;
|
||||
this.config.nodeExporter.enabled = !!this.featureStates.nodeExporter;
|
||||
|
||||
this.inProgress = true;
|
||||
|
||||
try {
|
||||
if (!this.config.prometheus.enabled && !this.config.kubeStateMetrics.enabled && !this.config.nodeExporter.enabled) {
|
||||
await this.feature.uninstall(this.config);
|
||||
} else {
|
||||
await this.feature.install(this.config);
|
||||
}
|
||||
} finally {
|
||||
this.inProgress = false;
|
||||
this.changed = false;
|
||||
|
||||
await this.updateFeatureStates();
|
||||
}
|
||||
}
|
||||
|
||||
async togglePrometheus(enabled: boolean) {
|
||||
this.featureStates.prometheus = enabled;
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
async toggleKubeStateMetrics(enabled: boolean) {
|
||||
this.featureStates.kubeStateMetrics = enabled;
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
async toggleNodeExporter(enabled: boolean) {
|
||||
this.featureStates.nodeExporter = enabled;
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
@computed get buttonLabel() {
|
||||
const allDisabled = !this.featureStates.kubeStateMetrics && !this.featureStates.nodeExporter && !this.featureStates.prometheus;
|
||||
|
||||
if (this.inProgress && this.canUpgrade) return "Upgrading ...";
|
||||
if (this.inProgress && allDisabled) return "Uninstalling ...";
|
||||
if (this.inProgress) return "Applying ...";
|
||||
if (this.canUpgrade) return "Upgrade";
|
||||
|
||||
if (this.changed && allDisabled) {
|
||||
return "Uninstall";
|
||||
}
|
||||
|
||||
return "Apply";
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{ !this.props.cluster.status.active && (
|
||||
<section>
|
||||
<p style={ {color: "var(--colorError)"} }>
|
||||
Lens Metrics settings requires established connection to the cluster.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
{ !this.isActiveMetricsProvider && (
|
||||
<section>
|
||||
<p style={ {color: "var(--colorError)"} }>
|
||||
Other metrics provider is currently active. See "Metrics" tab for details.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<Component.SubTitle title="Prometheus" />
|
||||
<Component.FormSwitch
|
||||
control={
|
||||
<Component.Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.prometheus && this.props.cluster.status.active}
|
||||
onChange={v => this.togglePrometheus(v.target.checked)}
|
||||
name="prometheus"
|
||||
/>
|
||||
}
|
||||
label="Enable bundled Prometheus metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Component.SubTitle title="Kube State Metrics" />
|
||||
<Component.FormSwitch
|
||||
control={
|
||||
<Component.Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.active}
|
||||
onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
label="Enable bundled kube-state-metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
Enable Kubernetes API object metrics for your cluster.
|
||||
Enable this only if you don't have existing kube-state-metrics stack installed.
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Component.SubTitle title="Node Exporter" />
|
||||
<Component.FormSwitch
|
||||
control={
|
||||
<Component.Switcher
|
||||
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.active}
|
||||
onChange={v => this.toggleNodeExporter(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
label="Enable bundled node-exporter stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
Enable node level metrics for your cluster.
|
||||
Enable this only if you don't have existing node-exporter stack installed.
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Component.Button
|
||||
label={this.buttonLabel}
|
||||
waiting={this.inProgress}
|
||||
onClick={() => this.save()}
|
||||
primary
|
||||
disabled={!this.changed} />
|
||||
|
||||
{this.canUpgrade && (<small className="hint">
|
||||
An update is available for enabled metrics components.
|
||||
</small>)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user