diff --git a/extensions/metrics-cluster-feature/renderer.tsx b/extensions/metrics-cluster-feature/renderer.tsx index 129e60ebff..f0d227f0a7 100644 --- a/extensions/metrics-cluster-feature/renderer.tsx +++ b/extensions/metrics-cluster-feature/renderer.tsx @@ -19,58 +19,24 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { LensRendererExtension, Interface, Component, Catalog} from "@k8slens/extensions"; -import { MetricsFeature } from "./src/metrics-feature"; +import React from "react"; +import { LensRendererExtension, Catalog } from "@k8slens/extensions"; +import { MetricsSettings } from "./src/metrics-settings"; export default class ClusterMetricsFeatureExtension extends LensRendererExtension { - onActivate() { - const category = Catalog.catalogCategories.getForGroupKind("entity.k8slens.dev", "KubernetesCluster"); - - if (!category) { - return; - } - - category.on("contextMenuOpen", this.clusterContextMenuOpen.bind(this)); - } - - async clusterContextMenuOpen(cluster: Catalog.KubernetesCluster, ctx: Interface.CatalogEntityContextMenuContext) { - if (!cluster.status.active) { - return; - } - - const metricsFeature = new MetricsFeature(); - - await metricsFeature.updateStatus(cluster); - - if (metricsFeature.status.installed) { - if (metricsFeature.status.canUpgrade) { - ctx.menuItems.unshift({ - icon: "refresh", - title: "Upgrade Lens Metrics stack", - onClick: async () => { - metricsFeature.upgrade(cluster); - } - }); + entitySettings = [ + { + apiVersions: ["entity.k8slens.dev/v1alpha1"], + kind: "KubernetesCluster", + title: "Lens Metrics", + priority: 5, + components: { + View: ({ entity = null }: { entity: Catalog.KubernetesCluster}) => { + return ( + + ); + } } - ctx.menuItems.unshift({ - icon: "toggle_off", - title: "Uninstall Lens Metrics stack", - onClick: async () => { - await metricsFeature.uninstall(cluster); - - Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 }); - } - }); - } else { - ctx.menuItems.unshift({ - icon: "toggle_on", - title: "Install Lens Metrics stack", - onClick: async () => { - metricsFeature.install(cluster); - - Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 }); - } - }); } - } + ]; } diff --git a/extensions/metrics-cluster-feature/resources/01-namespace.yml b/extensions/metrics-cluster-feature/resources/01-namespace.yml.hb similarity index 53% rename from extensions/metrics-cluster-feature/resources/01-namespace.yml rename to extensions/metrics-cluster-feature/resources/01-namespace.yml.hb index 85d13c1046..dd3816fdff 100644 --- a/extensions/metrics-cluster-feature/resources/01-namespace.yml +++ b/extensions/metrics-cluster-feature/resources/01-namespace.yml.hb @@ -2,3 +2,5 @@ apiVersion: v1 kind: Namespace metadata: name: lens-metrics + annotations: + extensionVersion: "{{ version }}" diff --git a/extensions/metrics-cluster-feature/resources/03-service.yml b/extensions/metrics-cluster-feature/resources/03-service.yml.hb similarity index 88% rename from extensions/metrics-cluster-feature/resources/03-service.yml rename to extensions/metrics-cluster-feature/resources/03-service.yml.hb index 1eb0cb5117..3cdcdbc260 100644 --- a/extensions/metrics-cluster-feature/resources/03-service.yml +++ b/extensions/metrics-cluster-feature/resources/03-service.yml.hb @@ -1,3 +1,4 @@ +{{#if prometheus.enabled}} apiVersion: v1 kind: Service metadata: @@ -14,3 +15,4 @@ spec: protocol: TCP port: 80 targetPort: 9090 +{{/if}} diff --git a/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb b/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb index dba437ee7d..ee68619a30 100644 --- a/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb +++ b/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb @@ -1,3 +1,4 @@ +{{#if prometheus.enabled}} apiVersion: apps/v1 kind: StatefulSet metadata: @@ -46,14 +47,14 @@ spec: serviceAccountName: prometheus initContainers: - name: chown - image: docker.io/alpine:3.9 + image: docker.io/alpine:3.12 command: ["chown", "-R", "65534:65534", "/var/lib/prometheus"] volumeMounts: - name: data mountPath: /var/lib/prometheus containers: - name: prometheus - image: quay.io/prometheus/prometheus:v2.19.3 + image: quay.io/prometheus/prometheus:v2.26.0 args: - --web.listen-address=0.0.0.0:9090 - --config.file=/etc/prometheus/prometheus.yaml @@ -114,3 +115,4 @@ spec: requests: storage: {{persistence.size}} {{/if}} +{{/if}} diff --git a/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb b/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb index 2c6786d816..2ff46d8d0b 100644 --- a/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb +++ b/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb @@ -41,7 +41,7 @@ spec: hostPID: true containers: - name: node-exporter - image: quay.io/prometheus/node-exporter:v1.0.1 + image: quay.io/prometheus/node-exporter:v1.1.2 args: - --path.procfs=/host/proc - --path.sysfs=/host/sys diff --git a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb index 763649f4f1..c895c9831c 100644 --- a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb +++ b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb @@ -39,7 +39,7 @@ spec: serviceAccountName: kube-state-metrics containers: - name: kube-state-metrics - image: quay.io/coreos/kube-state-metrics:v1.9.7 + image: quay.io/coreos/kube-state-metrics:v1.9.8 ports: - name: metrics containerPort: 8080 @@ -52,7 +52,7 @@ spec: resources: requests: cpu: 10m - memory: 150Mi + memory: 32Mi limits: cpu: 200m memory: 150Mi diff --git a/extensions/metrics-cluster-feature/src/metrics-feature.ts b/extensions/metrics-cluster-feature/src/metrics-feature.ts index ed20801f19..655c8e60ed 100644 --- a/extensions/metrics-cluster-feature/src/metrics-feature.ts +++ b/extensions/metrics-cluster-feature/src/metrics-feature.ts @@ -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 { + 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 { // 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 { - 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 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 { - 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 new file mode 100644 index 0000000000..f98cb66403 --- /dev/null +++ b/extensions/metrics-cluster-feature/src/metrics-settings.tsx @@ -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 { + @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 && ( +
+

+ Lens Metrics settings requires established connection to the cluster. +

+
+ )} + { !this.isActiveMetricsProvider && ( +
+

+ Other metrics provider is currently active. See "Metrics" tab for details. +

+
+ )} +
+ + this.togglePrometheus(v.target.checked)} + name="prometheus" + /> + } + label="Enable bundled Prometheus metrics stack" + /> + + Enable timeseries data visualization (Prometheus stack) for your cluster. + +
+ +
+ + this.toggleKubeStateMetrics(v.target.checked)} + name="node-exporter" + /> + } + label="Enable bundled kube-state-metrics stack" + /> + + Enable Kubernetes API object metrics for your cluster. + Enable this only if you don't have existing kube-state-metrics stack installed. + +
+ +
+ + this.toggleNodeExporter(v.target.checked)} + name="node-exporter" + /> + } + label="Enable bundled node-exporter stack" + /> + + Enable node level metrics for your cluster. + Enable this only if you don't have existing node-exporter stack installed. + +
+ +
+ this.save()} + primary + disabled={!this.changed} /> + + {this.canUpgrade && ( + An update is available for enabled metrics components. + )} +
+ + ); + } +} diff --git a/extensions/metrics-cluster-feature/tsconfig.json b/extensions/metrics-cluster-feature/tsconfig.json index a93ad6fe9f..016d32b0ba 100644 --- a/extensions/metrics-cluster-feature/tsconfig.json +++ b/extensions/metrics-cluster-feature/tsconfig.json @@ -16,8 +16,8 @@ "jsx": "react" }, "include": [ - "./*.ts", - "./*.tsx" + "./**/*.ts", + "./**/*.tsx" ], "exclude": [ "node_modules", diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 7c6727ada5..43fee50eaf 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -28,9 +28,24 @@ import { productName } from "../vars"; import { CatalogCategory, CatalogCategorySpec } from "../catalog"; import { app } from "electron"; + +export type KubernetesClusterPrometheusMetrics = { + address?: { + namespace: string; + service: string; + port: number; + prefix: string; + }; + type?: string; +}; + export type KubernetesClusterSpec = { kubeconfigPath: string; kubeconfigContext: string; + metrics?: { + source: string; + prometheus?: KubernetesClusterPrometheusMetrics; + } }; export interface KubernetesClusterStatus extends CatalogEntityStatus { @@ -88,7 +103,6 @@ export class KubernetesCluster extends CatalogEntity context.navigate(`/entity/${this.metadata.uid}/settings`) @@ -97,7 +111,6 @@ export class KubernetesCluster extends CatalogEntity ClusterStore.getInstance().removeById(this.metadata.uid), @@ -108,14 +121,20 @@ export class KubernetesCluster extends CatalogEntity { ClusterStore.getInstance().deactivate(this.metadata.uid); requestMain(clusterDisconnectHandler, this.metadata.uid); } }); + } else { + context.menuItems.push({ + title: "Connect", + onClick: async () => { + context.navigate(`/cluster/${this.metadata.uid}`); + } + }); } const category = catalogCategoryRegistry.getCategoryForEntity(this); diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index ee80b91786..51660a3d3d 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -83,7 +83,6 @@ export interface CatalogEntityActionContext { } export interface CatalogEntityContextMenu { - icon: string; title: string; onlyVisibleForSource?: string; // show only if empty or if matches with entity source onClick: () => void | Promise; @@ -92,6 +91,10 @@ export interface CatalogEntityContextMenu { } } +export interface CatalogEntityAddMenu extends CatalogEntityContextMenu { + icon: string; +} + export interface CatalogEntitySettingsMenu { group?: string; title: string; @@ -111,7 +114,7 @@ export interface CatalogEntitySettingsContext { export interface CatalogEntityAddMenuContext { navigate: (url: string) => void; - menuItems: CatalogEntityContextMenu[]; + menuItems: CatalogEntityAddMenu[]; } export type CatalogEntitySpec = Record; diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index d308de3f41..863ddafcd0 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -31,6 +31,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) => { @@ -67,14 +68,39 @@ if (ipcMain) { } }); - handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => { + handleRequest(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => { appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}); const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { const applier = new ResourceApplier(cluster); - applier.kubectlApplyAll(resources); + try { + const stdout = await applier.kubectlApplyAll(resources, extraArgs); + + return { stdout }; + } catch (error: any) { + return { stderr: error }; + } + } else { + throw `${clusterId} is not a valid cluster id`; + } + }); + + handleRequest(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: 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.kubectlDeleteAll(resources, extraArgs); + + 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..01c7ee84b8 --- /dev/null +++ b/src/common/k8s/resource-stack.ts @@ -0,0 +1,152 @@ +/** + * 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 fse from "fs-extra"; +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"; +import yaml from "js-yaml"; +import { productName } from "../vars"; + +export class ResourceStack { + constructor(protected cluster: KubernetesCluster, protected name: string) {} + + /** + * + * @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, extraArgs?: string[]): Promise { + const resources = await this.renderTemplates(folderPath, templateContext); + + return this.applyResources(resources, extraArgs); + } + + /** + * + * @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, extraArgs?: string[]): Promise { + const resources = await this.renderTemplates(folderPath, templateContext); + + return this.deleteResources(resources, extraArgs); + } + + protected async applyResources(resources: string[], extraArgs?: string[]): Promise { + const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid); + + if (!clusterModel) { + throw new Error(`cluster not found`); + } + + let kubectlArgs = extraArgs || []; + + kubectlArgs = this.appendKubectlArgs(kubectlArgs); + + if (app) { + return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs); + } else { + const response = await requestMain(clusterKubectlApplyAllHandler, this.cluster.metadata.uid, resources, kubectlArgs); + + if (response.stderr) { + throw new Error(response.stderr); + } + + return response.stdout; + } + } + + protected async deleteResources(resources: string[], extraArgs?: string[]): Promise { + const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid); + + if (!clusterModel) { + throw new Error(`cluster not found`); + } + + let kubectlArgs = extraArgs || []; + + kubectlArgs = this.appendKubectlArgs(kubectlArgs); + + if (app) { + return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs); + } else { + const response = await requestMain(clusterKubectlDeleteAllHandler, this.cluster.metadata.uid, resources, kubectlArgs); + + if (response.stderr) { + throw new Error(response.stderr); + } + + return response.stdout; + } + } + + protected appendKubectlArgs(kubectlArgs: string[]) { + if (!kubectlArgs.includes("-l") && !kubectlArgs.includes("--label")) { + return kubectlArgs.concat(["-l", `app.kubernetes.io/name=${this.name}`]); + } + + return kubectlArgs; + } + + protected async renderTemplates(folderPath: string, templateContext: any): Promise { + const resources: string[] = []; + + logger.info(`[RESOURCE-STACK]: render templates from ${folderPath}`); + const files = await fse.readdir(folderPath); + + for(const filename of files) { + const file = path.join(folderPath, filename); + const raw = await fse.readFile(file); + let resourceData: string; + + if (filename.endsWith(".hb")) { + const template = hb.compile(raw.toString()); + + resourceData = template(templateContext); + } else { + resourceData = raw.toString(); + } + + if (!resourceData.trim()) continue; + + const resourceArray = yaml.safeLoadAll(resourceData.toString()); + + resourceArray.forEach((resource) => { + if (resource?.metadata) { + resource.metadata.labels ||= {}; + resource.metadata.labels["app.kubernetes.io/name"] = this.name; + resource.metadata.labels["app.kubernetes.io/managed-by"] = productName; + resource.metadata.labels["app.kubernetes.io/created-by"] = "resource-stack"; + } + + resources.push(yaml.safeDump(resource)); + }); + } + + return resources; + } +} diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts deleted file mode 100644 index 2a1c289076..0000000000 --- a/src/extensions/cluster-feature.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * 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 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) { - await new ResourceApplier(clusterModel).kubectlApplyAll(resources); - } else { - 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 25608d69ba..0000000000 --- a/src/extensions/core-api/cluster-feature.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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. - */ - -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 4fdca344d9..56e4076ddb 100644 --- a/src/extensions/core-api/index.ts +++ b/src/extensions/core-api/index.ts @@ -28,7 +28,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"; import * as Types from "./types"; @@ -37,7 +36,6 @@ export { App, EventBus, Catalog, - ClusterFeature, Interface, Store, Types, diff --git a/src/extensions/registries/entity-setting-registry.ts b/src/extensions/registries/entity-setting-registry.ts index f2de7a3876..4854b97006 100644 --- a/src/extensions/registries/entity-setting-registry.ts +++ b/src/extensions/registries/entity-setting-registry.ts @@ -32,13 +32,14 @@ export interface EntitySettingComponents { } export interface EntitySettingRegistration { - title: string; - kind: string; apiVersions: string[]; - source?: string; + kind: string; + title: string; components: EntitySettingComponents; + source?: string; id?: string; priority?: number; + group?: string; } export interface RegisteredEntitySetting extends EntitySettingRegistration { diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts index f2648a1b3c..110b40bbb2 100644 --- a/src/extensions/renderer-api/components.ts +++ b/src/extensions/renderer-api/components.ts @@ -30,6 +30,7 @@ export * from "../../renderer/components/checkbox"; export * from "../../renderer/components/radio"; export * from "../../renderer/components/select"; export * from "../../renderer/components/slider"; +export * from "../../renderer/components/switch"; export * from "../../renderer/components/input/input"; // command-overlay diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts index f7d25a48b9..511597ba57 100644 --- a/src/extensions/renderer-api/k8s-api.ts +++ b/src/extensions/renderer-api/k8s-api.ts @@ -20,6 +20,7 @@ */ 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/cluster-manager.ts b/src/main/cluster-manager.ts index eae7ea68ba..0d8cd3535c 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -29,7 +29,7 @@ import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; import { Singleton } from "../common/utils"; import { catalogEntityRegistry } from "../common/catalog"; -import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster"; +import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster"; export class ClusterManager extends Singleton { constructor() { @@ -68,7 +68,7 @@ export class ClusterManager extends Singleton { const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id); if (index !== -1) { - const entity = catalogEntityRegistry.items[index]; + const entity = catalogEntityRegistry.items[index] as KubernetesCluster; entity.status.phase = cluster.disconnected ? "disconnected" : "connected"; entity.status.active = !cluster.disconnected; @@ -76,6 +76,17 @@ export class ClusterManager extends Singleton { if (cluster.preferences?.clusterName) { entity.metadata.name = cluster.preferences.clusterName; } + + entity.spec.metrics ||= { source: "local" }; + + if (entity.spec.metrics.source === "local") { + const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {}; + + prometheus.type = cluster.preferences.prometheusProvider?.type; + prometheus.address = cluster.preferences.prometheus; + entity.spec.metrics.prometheus = prometheus; + } + catalogEntityRegistry.items.splice(index, 1, entity); } } diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index beebd8cea5..f1c64f388c 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -73,7 +73,15 @@ export class ResourceApplier { }); } - public async kubectlApplyAll(resources: string[]): Promise { + public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise { + return this.kubectlCmdAll("apply", resources, extraArgs); + } + + public async kubectlDeleteAll(resources: string[], extraArgs?: string[]): Promise { + return this.kubectlCmdAll("delete", resources, extraArgs); + } + + protected async kubectlCmdAll(subCmd: string, resources: string[], args: string[] = []): Promise { const { kubeCtl } = this.cluster; const kubectlPath = await kubeCtl.getPath(); const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); @@ -85,19 +93,24 @@ 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}"`; + args.push("-f", `"${tmpDir}"`); + const cmd = `"${kubectlPath}" ${subCmd} --kubeconfig "${proxyKubeconfigPath}" ${args.join(" ")}`; - console.log("shooting manifests with:", cmd); - exec(cmd, (error, stdout, stderr) => { + logger.info(`[RESOURCE-APPLIER] running cmd ${cmd}`); + exec(cmd, (error, stdout) => { if (error) { - reject(`Error applying manifests:${error}`); - } + logger.error(`[RESOURCE-APPLIER] cmd errored: ${error}`); + const splitError = error.toString().split(`.yaml": `); - if (stderr != "") { - reject(stderr); + if (splitError[1]) { + reject(splitError[1]); + } else { + reject(error); + } return; } + resolve(stdout); }); }); diff --git a/src/renderer/api/catalog-entity.ts b/src/renderer/api/catalog-entity.ts index 77517fb168..493099d6c8 100644 --- a/src/renderer/api/catalog-entity.ts +++ b/src/renderer/api/catalog-entity.ts @@ -30,6 +30,7 @@ export { CatalogEntityKindData, CatalogEntityActionContext, CatalogEntityAddMenuContext, + CatalogEntityAddMenu, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../common/catalog"; diff --git a/src/renderer/components/+catalog/catalog-add-button.tsx b/src/renderer/components/+catalog/catalog-add-button.tsx index bfd5f9aa6e..5329702832 100644 --- a/src/renderer/components/+catalog/catalog-add-button.tsx +++ b/src/renderer/components/+catalog/catalog-add-button.tsx @@ -26,7 +26,7 @@ import { Icon } from "../icon"; import { disposeOnUnmount, observer } from "mobx-react"; import { observable, reaction } from "mobx"; import { autobind } from "../../../common/utils"; -import { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityContextMenu } from "../../api/catalog-entity"; +import { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity"; import { EventEmitter } from "events"; import { navigate } from "../../navigation"; @@ -37,7 +37,7 @@ export type CatalogAddButtonProps = { @observer export class CatalogAddButton extends React.Component { @observable protected isOpen = false; - protected menuItems = observable.array([]); + protected menuItems = observable.array([]); componentDidMount() { disposeOnUnmount(this, [ diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 3f0505d3c1..63239eb5d1 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -29,7 +29,6 @@ import { navigate } from "../../navigation"; import { kebabCase } from "lodash"; import { PageLayout } from "../layout/page-layout"; import { MenuItem, MenuActions } from "../menu"; -import { Icon } from "../icon"; import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity"; import { Badge } from "../badge"; import { HotbarStore } from "../../../common/hotbar-store"; @@ -136,16 +135,16 @@ export class Catalog extends React.Component { return ( item.onContextMenuOpen(this.contextMenu)}> - this.addToHotbar(item) }> - Pin to Hotbar - { menuItems.map((menuItem, index) => ( this.onMenuItemClick(menuItem)}> - {menuItem.title} + {menuItem.title} )) } + this.addToHotbar(item) }> + Pin to Hotbar + ); } diff --git a/src/renderer/components/+entity-settings/entity-settings.tsx b/src/renderer/components/+entity-settings/entity-settings.tsx index 7719b7b45d..779cce45d6 100644 --- a/src/renderer/components/+entity-settings/entity-settings.tsx +++ b/src/renderer/components/+entity-settings/entity-settings.tsx @@ -32,6 +32,7 @@ import { CatalogEntity } from "../../api/catalog-entity"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { entitySettingRegistry } from "../../../extensions/registries"; import { EntitySettingsRouteParams } from "./entity-settings.route"; +import { groupBy } from "lodash"; interface Props extends RouteComponentProps { } @@ -57,9 +58,15 @@ export class EntitySettings extends React.Component { async componentDidMount() { const { hash } = navigation.location; - this.ensureActiveTab(); + if (hash) { + const item = this.menuItems.find((item) => item.title === hash.slice(1)); - document.getElementById(hash.slice(1))?.scrollIntoView(); + if (item) { + this.activeTab = item.id; + } + } + + this.ensureActiveTab(); } onTabChange = (tabId: string) => { @@ -67,18 +74,24 @@ export class EntitySettings extends React.Component { }; renderNavigation() { + const groups = Object.entries(groupBy(this.menuItems, (item) => item.group || "Extensions")); + return ( <>

{this.entity.metadata.name}

-
Settings
- { this.menuItems.map((setting) => ( - + { groups.map((group) => ( + <> +
{group[0]}
+ { group[1].map((setting, index) => ( + + ))} + ))}
@@ -111,7 +124,7 @@ export class EntitySettings extends React.Component {

{activeSetting.title}

- +
diff --git a/src/renderer/components/cluster-settings/cluster-settings.tsx b/src/renderer/components/cluster-settings/cluster-settings.tsx index 9d029388d8..d0aaf18e5c 100644 --- a/src/renderer/components/cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/cluster-settings/cluster-settings.tsx @@ -43,6 +43,7 @@ entitySettingRegistry.add([ kind: "KubernetesCluster", source: "local", title: "General", + group: "Settings", components: { View: (props: { entity: CatalogEntity }) => { const cluster = getClusterForEntity(props.entity); @@ -68,6 +69,7 @@ entitySettingRegistry.add([ apiVersions: ["entity.k8slens.dev/v1alpha1"], kind: "KubernetesCluster", title: "Proxy", + group: "Settings", components: { View: (props: { entity: CatalogEntity }) => { const cluster = getClusterForEntity(props.entity); @@ -88,6 +90,7 @@ entitySettingRegistry.add([ apiVersions: ["entity.k8slens.dev/v1alpha1"], kind: "KubernetesCluster", title: "Terminal", + group: "Settings", components: { View: (props: { entity: CatalogEntity }) => { const cluster = getClusterForEntity(props.entity); @@ -108,6 +111,7 @@ entitySettingRegistry.add([ apiVersions: ["entity.k8slens.dev/v1alpha1"], kind: "KubernetesCluster", title: "Namespaces", + group: "Settings", components: { View: (props: { entity: CatalogEntity }) => { const cluster = getClusterForEntity(props.entity); @@ -128,6 +132,7 @@ entitySettingRegistry.add([ apiVersions: ["entity.k8slens.dev/v1alpha1"], kind: "KubernetesCluster", title: "Metrics", + group: "Settings", components: { View: (props: { entity: CatalogEntity }) => { const cluster = getClusterForEntity(props.entity); diff --git a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.scss b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.scss deleted file mode 100644 index e6a2ed51c7..0000000000 --- a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.scss +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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. - */ - -.MetricsSelect { - $spacing: $padding; - --flex-gap: #{$spacing}; - - .Badge { - margin-top: $spacing; - } - - .Button { - margin-top: $spacing; - } -} diff --git a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx index 8c9f40f71d..bfa4630032 100644 --- a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx @@ -19,8 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./cluster-metrics-setting.scss"; - import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { Select, SelectOption } from "../../select/select"; diff --git a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx index 4c65506083..36395f1bdb 100644 --- a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx @@ -27,6 +27,7 @@ import { SubTitle } from "../../layout/sub-title"; import { Select, SelectOption } from "../../select"; import { Input } from "../../input"; import { observable, computed, autorun } from "mobx"; +import { productName } from "../../../../common/vars"; const options: SelectOption[] = [ { value: "", label: "Auto detect" }, @@ -102,23 +103,20 @@ export class ClusterPrometheusSetting extends React.Component { render() { return ( <> - -

- Use pre-installed Prometheus service for metrics. Please refer to the{" "} - guide{" "} - for possible configuration changes. -

- { + this.provider = value; + this.onSaveProvider(); + }} + options={options} + /> + What query format is used to fetch metrics from Prometheus + {this.canEditPrometheusPath && ( - <> +

Prometheus service address.

{ /> An address to an existing Prometheus installation{" "} - ({"/:"}). Lens tries to auto-detect address if left empty. + ({"/:"}). {productName} tries to auto-detect address if left empty. - +
)} ); diff --git a/src/renderer/components/cluster-settings/components/remove-cluster-button.tsx b/src/renderer/components/cluster-settings/components/remove-cluster-button.tsx deleted file mode 100644 index fc3e683c17..0000000000 --- a/src/renderer/components/cluster-settings/components/remove-cluster-button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * 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 { observer } from "mobx-react"; -import { ClusterStore } from "../../../../common/cluster-store"; -import { Cluster } from "../../../../main/cluster"; -import { autobind } from "../../../utils"; -import { Button } from "../../button"; -import { ConfirmDialog } from "../../confirm-dialog"; - -interface Props { - cluster: Cluster; -} - -@observer -export class RemoveClusterButton extends React.Component { - @autobind() - confirmRemoveCluster() { - const { cluster } = this.props; - - ConfirmDialog.open({ - message:

Are you sure you want to remove {cluster.preferences.clusterName} from Lens?

, - labelOk: "Yes", - labelCancel: "No", - ok: async () => { - await ClusterStore.getInstance().removeById(cluster.id); - } - }); - } - - render() { - return ( - - ); - } -} diff --git a/src/renderer/components/cluster-settings/components/show-metrics.tsx b/src/renderer/components/cluster-settings/components/show-metrics.tsx index 3ac4a237ef..e744875810 100644 --- a/src/renderer/components/cluster-settings/components/show-metrics.tsx +++ b/src/renderer/components/cluster-settings/components/show-metrics.tsx @@ -19,8 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./cluster-metrics-setting.scss"; - import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { Cluster } from "../../../../main/cluster"; diff --git a/src/renderer/components/drawer/drawer.scss b/src/renderer/components/drawer/drawer.scss index 4c972bdd3c..47168df569 100644 --- a/src/renderer/components/drawer/drawer.scss +++ b/src/renderer/components/drawer/drawer.scss @@ -83,6 +83,10 @@ .MenuActions.toolbar .Icon { color: $drawerTitleText; } + + .Menu { + box-shadow: none; + } } .drawer-content { @@ -99,4 +103,4 @@ width: var(--full-size) } } -} \ No newline at end of file +} diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index e71ab2fdb2..41b0a69da0 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -105,13 +105,11 @@ export class HotbarEntityIcon extends React.Component { if (!isPersisted) { menuItems.unshift({ title: "Pin to Hotbar", - icon: "push_pin", onClick: () => add(entity, index) }); } else { menuItems.unshift({ title: "Unpin from Hotbar", - icon: "push_pin", onClick: () => remove(entity.metadata.uid) }); } diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 2f681a05c1..ddfa512dcd 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -29,7 +29,6 @@ import GraphemeSplitter from "grapheme-splitter"; import { CatalogEntityContextMenu } from "../../../common/catalog"; import { cssNames, IClassName, iter } from "../../utils"; import { ConfirmDialog } from "../confirm-dialog"; -import { Icon } from "../icon"; import { Menu, MenuItem } from "../menu"; import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip"; import { observer } from "mobx-react"; @@ -137,7 +136,7 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { { menuItems.map((menuItem) => { return ( onMenuItemClick(menuItem) }> - {menuItem.title} + {menuItem.title} ); })} diff --git a/src/renderer/components/menu/menu.scss b/src/renderer/components/menu/menu.scss index 6682d2ad60..d94cbff63e 100644 --- a/src/renderer/components/menu/menu.scss +++ b/src/renderer/components/menu/menu.scss @@ -20,7 +20,7 @@ */ .Menu { - --bgc: #{$contentColor}; + --bgc: #{$layoutBackground}; position: absolute; display: flex; @@ -29,6 +29,8 @@ list-style: none; border: 1px solid $borderColor; z-index: 101; + box-shadow: rgba(0,0,0,0.24) 0px 8px 16px 0px; + border-radius: 4px; &.portal { left: -1000px;