Allow extensions to define cluster features (#1125)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2020-10-26 08:21:22 +02:00
1 parent f3a0059355
commit 62ae7771df
81 files changed
+4069 -510

No files matched your search

+7 -21
View File
@@ -2,6 +2,7 @@ import { createIpcChannel } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store";
import { extensionLoader } from "../extensions/extension-loader"
import { appEventBus } from "./event-bus"
import { ResourceApplier } from "../main/resource-applier";
export const clusterIpc = {
activate: createIpcChannel({
@@ -42,32 +43,17 @@ export const clusterIpc = {
},
}),
installFeature: createIpcChannel({
channel: "cluster:install-feature",
handle: async (clusterId: ClusterId, feature: string, config?: any) => {
appEventBus.emit({name: "cluster", action: "install", params: { feature: feature}})
kubectlApplyAll: createIpcChannel({
channel: "cluster:kubectl-apply-all",
handle: (clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
const cluster = clusterStore.getById(clusterId);
if (cluster) {
await cluster.installFeature(feature, config)
const applier = new ResourceApplier(cluster)
applier.kubectlApplyAll(resources)
} else {
throw `${clusterId} is not a valid cluster id`;
}
}
}),
uninstallFeature: createIpcChannel({
channel: "cluster:uninstall-feature",
handle: (clusterId: ClusterId, feature: string) => {
appEventBus.emit({name: "cluster", action: "uninstall", params: { feature: feature}})
return clusterStore.getById(clusterId)?.uninstallFeature(feature)
}
}),
upgradeFeature: createIpcChannel({
channel: "cluster:upgrade-feature",
handle: (clusterId: ClusterId, feature: string, config?: any) => {
appEventBus.emit({name: "cluster", action: "upgrade", params: { feature: feature}})
return clusterStore.getById(clusterId)?.upgradeFeature(feature, config)
}
}),
}
+62
View File
@@ -0,0 +1,62 @@
import fs from "fs";
import path from "path"
import hb from "handlebars"
import { observable } from "mobx"
import { ResourceApplier } from "../main/resource-applier"
import { Cluster } from "../main/cluster";
import logger from "../main/logger";
import { app } from "electron"
import { clusterIpc } from "../common/cluster-ipc"
export interface ClusterFeatureStatus {
currentVersion: string;
installed: boolean;
latestVersion: string;
canUpgrade: boolean;
}
export abstract class ClusterFeature {
name: string;
latestVersion: string;
config: any;
@observable status: ClusterFeatureStatus = {
currentVersion: null,
installed: false,
latestVersion: null,
canUpgrade: false
}
abstract async install(cluster: Cluster): Promise<void>;
abstract async upgrade(cluster: Cluster): Promise<void>;
abstract async uninstall(cluster: Cluster): Promise<void>;
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
protected async applyResources(cluster: Cluster, resources: string[]) {
if (app) {
await new ResourceApplier(cluster).kubectlApplyAll(resources)
} else {
await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources)
}
}
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.config));
} else {
resources.push(raw.toString());
}
});
return resources;
}
}
@@ -0,0 +1 @@
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
+2
View File
@@ -10,11 +10,13 @@ import * as Store from "./stores"
import * as Util from "./utils"
import * as Registry from "../registries"
import * as CommonVars from "../../common/vars";
import * as ClusterFeature from "./cluster-feature"
export let windowManager: WindowManager;
export {
EventBus,
ClusterFeature,
Store,
Util,
Registry,
+1
View File
@@ -1 +1,2 @@
export { ExtensionStore } from "../extension-store"
export type { Cluster } from "../../main/cluster"
+19
View File
@@ -0,0 +1,19 @@
// Lens-extensions api developer's kit
export type { LensExtensionRuntimeEnv } from "./lens-runtime";
export * from "./lens-main-extension"
export * from "./lens-renderer-extension"
// APIs
import * as EventBus from "./core-api/event-bus"
import * as Store from "./core-api/stores"
import * as Util from "./core-api/utils"
import * as Registry from "./core-api/registries"
import * as ClusterFeature from "./core-api/cluster-feature"
export {
ClusterFeature,
EventBus,
Registry,
Store,
Util
}
+10 -6
View File
@@ -6,7 +6,7 @@ import { broadcastIpc } from "../common/ipc"
import { observable, reaction, toJS, } from "mobx"
import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron"
import { appPreferenceRegistry, kubeObjectMenuRegistry, menuRegistry, pageRegistry, statusBarRegistry } from "./registries";
import { appPreferenceRegistry, kubeObjectMenuRegistry, menuRegistry, pageRegistry, statusBarRegistry, clusterFeatureRegistry } from "./registries";
export interface InstalledExtension extends ExtensionModel {
manifestPath: string;
@@ -46,6 +46,7 @@ export class ExtensionLoader {
this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerPages(pageRegistry)
instance.registerAppPreferences(appPreferenceRegistry)
instance.registerClusterFeatures(clusterFeatureRegistry)
instance.registerStatusBarIcon(statusBarRegistry)
})
}
@@ -60,8 +61,8 @@ export class ExtensionLoader {
protected autoloadExtensions(callback: (instance: LensExtension) => void) {
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for (const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.name)
for(const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.id)
if (!instance) {
const extensionModule = this.requireExtension(ext)
if (!extensionModule) {
@@ -69,9 +70,12 @@ export class ExtensionLoader {
}
const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
instance.enable();
callback(instance)
this.instances.set(ext.name, instance)
try {
instance.enable()
callback(instance)
} finally {
this.instances.set(ext.id, instance)
}
}
}
}, {
+5 -1
View File
@@ -1,5 +1,5 @@
import { LensExtension } from "./lens-extension"
import type { PageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry } from "./registries"
import type { PageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries"
export class LensRendererExtension extends LensExtension {
registerPages(registry: PageRegistry) {
@@ -10,6 +10,10 @@ export class LensRendererExtension extends LensExtension {
return
}
registerClusterFeatures(registry: ClusterFeatureRegistry) {
return
}
registerStatusBarIcon(registry: StatusBarRegistry) {
return
}
@@ -0,0 +1,16 @@
import { BaseRegistry } from "./base-registry";
import { ClusterFeature } from "../cluster-feature";
export interface ClusterFeatureComponents {
Description: React.ComponentType<any>;
}
export interface ClusterFeatureRegistration {
title: string;
components: ClusterFeatureComponents
feature: ClusterFeature
}
export class ClusterFeatureRegistry extends BaseRegistry<ClusterFeatureRegistration> {}
export const clusterFeatureRegistry = new ClusterFeatureRegistry()
+1
View File
@@ -5,3 +5,4 @@ export * from "./menu-registry"
export * from "./app-preference-registry"
export * from "./status-bar-registry"
export * from "./kube-object-menu-registry";
export * from "./cluster-feature-registry"
+1 -2
View File
@@ -1,6 +1,5 @@
export { apiManager } from "../../renderer/api/api-manager";
export { KubeApi } from "../../renderer/api/kube-api";
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object";
export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
export { Node, nodesApi } from "../../renderer/api/endpoints";
-100
View File
@@ -1,100 +0,0 @@
import { Feature, FeatureStatus } from "../main/feature"
import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node"
import semver from "semver"
import { Cluster } from "../main/cluster";
import * as k8s from "@kubernetes/client-node"
export interface MetricsConfiguration {
// Placeholder for Metrics config structure
persistence: {
enabled: boolean;
storageClass: string;
size: string;
};
nodeExporter: {
enabled: boolean;
};
kubeStateMetrics: {
enabled: boolean;
};
retention: {
time: string;
size: string;
};
alertManagers: string[];
replicas: number;
storageClass: string;
}
export class MetricsFeature extends Feature {
static id = 'metrics'
name = MetricsFeature.id;
latestVersion = "v2.17.2-lens1"
config: 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,
};
async install(cluster: Cluster): Promise<void> {
// Check if there are storageclasses
const storageClient = cluster.getProxyKubeconfig().makeApiClient(k8s.StorageV1Api)
const scs = await storageClient.listStorageClass();
this.config.persistence.enabled = scs.body.items.some(sc => (
sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true' ||
sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true'
));
return super.install(cluster)
}
async upgrade(cluster: Cluster): Promise<void> {
return this.install(cluster)
}
async featureStatus(kc: KubeConfig): Promise<FeatureStatus> {
const client = kc.makeApiClient(AppsV1Api)
const status: FeatureStatus = {
currentVersion: null,
installed: false,
latestVersion: this.latestVersion,
canUpgrade: false, // Dunno yet
};
try {
const prometheus = (await client.readNamespacedStatefulSet('prometheus', 'lens-metrics')).body;
status.installed = true;
status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
status.canUpgrade = semver.lt(status.currentVersion, this.latestVersion, true);
} catch {
// ignore error
}
return status;
}
async uninstall(cluster: Cluster): Promise<void> {
const rbacClient = cluster.getProxyKubeconfig().makeApiClient(RbacAuthorizationV1Api)
await this.deleteNamespace(cluster.getProxyKubeconfig(), "lens-metrics")
await rbacClient.deleteClusterRole("lens-prometheus");
await rbacClient.deleteClusterRoleBinding("lens-prometheus");
}
}
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: lens-metrics
-272
View File
@@ -1,272 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: lens-metrics
data:
prometheus.yaml: |-
# Global config
global:
scrape_interval: 15s
{{#if alertManagers}}
# AlertManager
alerting:
alertmanagers:
- static_configs:
- targets:
{{#each alertManagers}}
- {{this}}
{{/each}}
{{/if}}
# Scrape configs for running Prometheus on a Kubernetes cluster.
# This uses separate scrape configs for cluster components (i.e. API server, node)
# and services to allow each to use different authentication configs.
#
# Kubernetes labels will be added as Prometheus labels on metrics via the
# `labelmap` relabeling action.
scrape_configs:
# Scrape config for API servers.
#
# Kubernetes exposes API servers as endpoints to the default/kubernetes
# service so this uses `endpoints` role and uses relabelling to only keep
# the endpoints associated with the default/kubernetes service using the
# default named port `https`. This works for single API server deployments as
# well as HA API server deployments.
- job_name: 'kubernetes-apiservers'
kubernetes_sd_configs:
- role: endpoints
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Using endpoints to discover kube-apiserver targets finds the pod IP
# (host IP since apiserver uses host network) which is not used in
# the server certificate.
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
# Keep only the default/kubernetes service endpoints for the https port. This
# will add targets for each API server which Kubernetes adds an endpoint to
# the default/kubernetes service.
relabel_configs:
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: default;kubernetes;https
- replacement: apiserver
action: replace
target_label: job
# Scrape config for node (i.e. kubelet) /metrics (e.g. 'kubelet_'). Explore
# metrics from a node by scraping kubelet (127.0.0.1:10250/metrics).
- job_name: 'kubelet'
kubernetes_sd_configs:
- role: node
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Kubelet certs don't have any fixed IP SANs
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- replacement: 'lens-metrics'
target_label: kubernetes_namespace
metric_relabel_configs:
- source_labels:
- namespace
action: replace
regex: (.+)
target_label: kubernetes_namespace
# Scrape config for Kubelet cAdvisor. Explore metrics from a node by
# scraping kubelet (127.0.0.1:10250/metrics/cadvisor).
- job_name: 'kubernetes-cadvisor'
kubernetes_sd_configs:
- role: node
scheme: https
metrics_path: /metrics/cadvisor
tls_config:
# Kubelet certs don't have any fixed IP SANs
insecure_skip_verify: true
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
metric_relabel_configs:
- source_labels:
- namespace
action: replace
target_label: kubernetes_namespace
- source_labels:
- pod
regex: (.*)
replacement: $1
action: replace
target_label: pod_name
- source_labels:
- container
regex: (.*)
replacement: $1
action: replace
target_label: container_name
# Scrap etcd metrics from masters via etcd-scraper-proxy
- job_name: 'etcd'
kubernetes_sd_configs:
- role: pod
scheme: http
relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
action: keep
regex: 'kube-system'
- source_labels: [__meta_kubernetes_pod_label_component]
action: keep
regex: 'etcd-scraper-proxy'
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
# Scrape config for service endpoints.
#
# The relabeling allows the actual service scrape endpoint to be configured
# via the following annotations:
#
# * `prometheus.io/scrape`: Only scrape services that have a value of `true`
# * `prometheus.io/scheme`: If the metrics endpoint is secured then you will need
# to set this to `https` & most likely set the `tls_config` of the scrape config.
# * `prometheus.io/path`: If the metrics path is not `/metrics` override this.
# * `prometheus.io/port`: If the metrics are exposed on a different port to the
# service then set this appropriately.
- job_name: 'kubernetes-service-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
action: replace
target_label: __scheme__
regex: (https?)
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_service_name]
action: replace
target_label: job
- action: replace
source_labels:
- __meta_kubernetes_pod_node_name
target_label: kubernetes_node
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
metric_relabel_configs:
- source_labels:
- namespace
action: replace
regex: (.+)
target_label: kubernetes_namespace
# Example scrape config for probing services via the Blackbox Exporter.
#
# The relabeling allows the actual service scrape endpoint to be configured
# via the following annotations:
#
# * `prometheus.io/probe`: Only probe services that have a value of `true`
- job_name: 'kubernetes-services'
metrics_path: /probe
params:
module: [http_2xx]
kubernetes_sd_configs:
- role: service
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
action: keep
regex: true
- source_labels: [__address__]
target_label: __param_target
- target_label: __address__
replacement: blackbox
- source_labels: [__param_target]
target_label: instance
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_service_name]
target_label: job
metric_relabel_configs:
- source_labels:
- namespace
action: replace
regex: (.+)
target_label: kubernetes_namespace
# Example scrape config for pods
#
# The relabeling allows the actual pod scrape endpoint to be configured via the
# following annotations:
#
# * `prometheus.io/scrape`: Only scrape pods that have a value of `true`
# * `prometheus.io/path`: If the metrics path is not `/metrics` override this.
# * `prometheus.io/port`: Scrape the pod on the indicated port instead of the
# pod's declared ports (default is a port-free target if none are declared).
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name
metric_relabel_configs:
- source_labels:
- namespace
action: replace
regex: (.+)
target_label: kubernetes_namespace
# Rule files
rule_files:
- "/etc/prometheus/rules/*.rules"
- "/etc/prometheus/rules/*.yaml"
- "/etc/prometheus/rules/*.yml"
@@ -1,5 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
namespace: lens-metrics
-16
View File
@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: lens-metrics
annotations:
prometheus.io/scrape: 'true'
spec:
type: ClusterIP
selector:
name: prometheus
ports:
- name: web
protocol: TCP
port: 80
targetPort: 9090
-116
View File
@@ -1,116 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: prometheus
namespace: lens-metrics
spec:
replicas: {{replicas}}
serviceName: prometheus
selector:
matchLabels:
name: prometheus
template:
metadata:
labels:
name: prometheus
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
# <%- if config.node_selector -%>
# nodeSelector:
# <%- node_selector.to_h.each do |key, value| -%>
# <%= key %>: <%= value %>
# <%- end -%>
# <%- end -%>
# <%- unless config.tolerations.empty? -%>
# tolerations:
# <%- config.tolerations.each do |t| -%>
# -
# <%- t.each do |k,v| -%>
# <%= k %>: <%= v %>
# <%- end -%>
# <%- end -%>
# <%- end -%>
serviceAccountName: prometheus
initContainers:
- name: chown
image: docker.io/alpine:3.9
command: ["chown", "-R", "65534:65534", "/var/lib/prometheus"]
volumeMounts:
- name: data
mountPath: /var/lib/prometheus
containers:
- name: prometheus
image: docker.io/prom/prometheus:v2.17.2
args:
- --web.listen-address=0.0.0.0:9090
- --config.file=/etc/prometheus/prometheus.yaml
- --storage.tsdb.path=/var/lib/prometheus
- --storage.tsdb.retention.time={{retention.time}}
- --storage.tsdb.retention.size={{retention.size}}
- --storage.tsdb.min-block-duration=2h
- --storage.tsdb.max-block-duration=2h
ports:
- name: web
containerPort: 9090
volumeMounts:
- name: config
mountPath: /etc/prometheus
- name: rules
mountPath: /etc/prometheus/rules
- name: data
mountPath: /var/lib/prometheus
readinessProbe:
httpGet:
path: /-/ready
port: 9090
initialDelaySeconds: 10
timeoutSeconds: 10
livenessProbe:
httpGet:
path: /-/healthy
port: 9090
initialDelaySeconds: 10
timeoutSeconds: 10
resources:
requests:
cpu: 100m
memory: 512Mi
terminationGracePeriodSeconds: 30
volumes:
- name: config
configMap:
name: prometheus-config
- name: rules
configMap:
name: prometheus-rules
{{#unless persistence.enabled}}
- name: data
emptyDir: {}
{{/unless}}
{{#if persistence.enabled}}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
{{#if persistence.storageClass}}
storageClassName: "{{persistence.storageClass}}"
{{/if}}
resources:
requests:
storage: {{persistence.size}}
{{/if}}
-514
View File
@@ -1,514 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-rules
namespace: lens-metrics
data:
alertmanager.rules.yaml: |
groups:
- name: alertmanager.rules
rules:
- alert: AlertmanagerConfigInconsistent
expr: count_values("config_hash", alertmanager_config_hash) BY (service) / ON(service)
GROUP_LEFT() label_replace(prometheus_operator_alertmanager_spec_replicas, "service",
"alertmanager-$1", "alertmanager", "(.*)") != 1
for: 5m
labels:
severity: critical
annotations:
description: The configuration of the instances of the Alertmanager cluster
`{{$labels.service}}` are out of sync.
- alert: AlertmanagerDownOrMissing
expr: label_replace(prometheus_operator_alertmanager_spec_replicas, "job", "alertmanager-$1",
"alertmanager", "(.*)") / ON(job) GROUP_RIGHT() sum(up) BY (job) != 1
for: 5m
labels:
severity: warning
annotations:
description: An unexpected number of Alertmanagers are scraped or Alertmanagers
disappeared from discovery.
- alert: AlertmanagerFailedReload
expr: alertmanager_config_last_reload_successful == 0
for: 10m
labels:
severity: warning
annotations:
description: Reloading Alertmanager's configuration has failed for {{ $labels.namespace
}}/{{ $labels.pod}}.
etcd3.rules.yaml: |
groups:
- name: ./etcd3.rules
rules:
- alert: InsufficientMembers
expr: count(up{job="etcd"} == 0) > (count(up{job="etcd"}) / 2 - 1)
for: 3m
labels:
severity: critical
annotations:
description: If one more etcd member goes down the cluster will be unavailable
summary: etcd cluster insufficient members
- alert: NoLeader
expr: etcd_server_has_leader{job="etcd"} == 0
for: 1m
labels:
severity: critical
annotations:
description: etcd member {{ $labels.instance }} has no leader
summary: etcd member has no leader
- alert: HighNumberOfLeaderChanges
expr: increase(etcd_server_leader_changes_seen_total{job="etcd"}[1h]) > 3
labels:
severity: warning
annotations:
description: etcd instance {{ $labels.instance }} has seen {{ $value }} leader
changes within the last hour
summary: a high number of leader changes within the etcd cluster are happening
- alert: GRPCRequestsSlow
expr: histogram_quantile(0.99, sum(rate(grpc_server_handling_seconds_bucket{job="etcd",grpc_type="unary"}[5m])) by (grpc_service, grpc_method, le))
> 0.15
for: 10m
labels:
severity: critical
annotations:
description: on etcd instance {{ $labels.instance }} gRPC requests to {{ $labels.grpc_method
}} are slow
summary: slow gRPC requests
- alert: HighNumberOfFailedHTTPRequests
expr: sum(rate(etcd_http_failed_total{job="etcd"}[5m])) BY (method) / sum(rate(etcd_http_received_total{job="etcd"}[5m]))
BY (method) > 0.01
for: 10m
labels:
severity: warning
annotations:
description: '{{ $value }}% of requests for {{ $labels.method }} failed on etcd
instance {{ $labels.instance }}'
summary: a high number of HTTP requests are failing
- alert: HighNumberOfFailedHTTPRequests
expr: sum(rate(etcd_http_failed_total{job="etcd"}[5m])) BY (method) / sum(rate(etcd_http_received_total{job="etcd"}[5m]))
BY (method) > 0.05
for: 5m
labels:
severity: critical
annotations:
description: '{{ $value }}% of requests for {{ $labels.method }} failed on etcd
instance {{ $labels.instance }}'
summary: a high number of HTTP requests are failing
- alert: HTTPRequestsSlow
expr: histogram_quantile(0.99, rate(etcd_http_successful_duration_seconds_bucket[5m]))
> 0.15
for: 10m
labels:
severity: warning
annotations:
description: on etcd instance {{ $labels.instance }} HTTP requests to {{ $labels.method
}} are slow
summary: slow HTTP requests
- alert: EtcdMemberCommunicationSlow
expr: histogram_quantile(0.99, rate(etcd_network_peer_round_trip_time_seconds_bucket[5m]))
> 0.15
for: 10m
labels:
severity: warning
annotations:
description: etcd instance {{ $labels.instance }} member communication with
{{ $labels.To }} is slow
summary: etcd member communication is slow
- alert: HighNumberOfFailedProposals
expr: increase(etcd_server_proposals_failed_total{job="etcd"}[1h]) > 5
labels:
severity: warning
annotations:
description: etcd instance {{ $labels.instance }} has seen {{ $value }} proposal
failures within the last hour
summary: a high number of proposals within the etcd cluster are failing
- alert: HighFsyncDurations
expr: histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))
> 0.5
for: 10m
labels:
severity: warning
annotations:
description: etcd instance {{ $labels.instance }} fync durations are high
summary: high fsync durations
- alert: HighCommitDurations
expr: histogram_quantile(0.99, rate(etcd_disk_backend_commit_duration_seconds_bucket[5m]))
> 0.25
for: 10m
labels:
severity: warning
annotations:
description: etcd instance {{ $labels.instance }} commit durations are high
summary: high commit durations
general.rules.yaml: |
groups:
- name: general.rules
rules:
- alert: TargetDown
expr: 100 * (count(up == 0) BY (job) / count(up) BY (job)) > 10
for: 10m
labels:
severity: warning
annotations:
description: '{{ $value }}% of {{ $labels.job }} targets are down.'
summary: Targets are down
- record: fd_utilization
expr: process_open_fds / process_max_fds
- alert: FdExhaustionClose
expr: predict_linear(fd_utilization[1h], 3600 * 4) > 1
for: 10m
labels:
severity: warning
annotations:
description: '{{ $labels.job }}: {{ $labels.namespace }}/{{ $labels.pod }} instance
will exhaust in file/socket descriptors within the next 4 hours'
summary: file descriptors soon exhausted
- alert: FdExhaustionClose
expr: predict_linear(fd_utilization[10m], 3600) > 1
for: 10m
labels:
severity: critical
annotations:
description: '{{ $labels.job }}: {{ $labels.namespace }}/{{ $labels.pod }} instance
will exhaust in file/socket descriptors within the next hour'
summary: file descriptors soon exhausted
kube-state-metrics.rules.yaml: |
groups:
- name: kube-state-metrics.rules
rules:
- alert: DeploymentGenerationMismatch
expr: kube_deployment_status_observed_generation != kube_deployment_metadata_generation
for: 15m
labels:
severity: warning
annotations:
description: Observed deployment generation does not match expected one for
deployment {{$labels.namespaces}}/{{$labels.deployment}}
summary: Deployment is outdated
- alert: DeploymentReplicasNotUpdated
expr: ((kube_deployment_status_replicas_updated != kube_deployment_spec_replicas)
or (kube_deployment_status_replicas_available != kube_deployment_spec_replicas))
unless (kube_deployment_spec_paused == 1)
for: 15m
labels:
severity: warning
annotations:
description: Replicas are not updated and available for deployment {{$labels.namespaces}}/{{$labels.deployment}}
summary: Deployment replicas are outdated
- alert: DaemonSetRolloutStuck
expr: kube_daemonset_status_number_ready / kube_daemonset_status_desired_number_scheduled
* 100 < 100
for: 15m
labels:
severity: warning
annotations:
description: Only {{$value}}% of desired pods scheduled and ready for daemon
set {{$labels.namespaces}}/{{$labels.daemonset}}
summary: DaemonSet is missing pods
- alert: K8SDaemonSetsNotScheduled
expr: kube_daemonset_status_desired_number_scheduled - kube_daemonset_status_current_number_scheduled
> 0
for: 10m
labels:
severity: warning
annotations:
description: A number of daemonsets are not scheduled.
summary: Daemonsets are not scheduled correctly
- alert: DaemonSetsMissScheduled
expr: kube_daemonset_status_number_misscheduled > 0
for: 10m
labels:
severity: warning
annotations:
description: A number of daemonsets are running where they are not supposed
to run.
summary: Daemonsets are not scheduled correctly
- alert: PodFrequentlyRestarting
expr: increase(kube_pod_container_status_restarts_total[1h]) > 5
for: 10m
labels:
severity: warning
annotations:
description: Pod {{$labels.namespaces}}/{{$labels.pod}} restarted {{$value}}
times within the last hour
summary: Pod is restarting frequently
kubelet.rules.yaml: |
groups:
- name: kubelet.rules
rules:
- alert: K8SNodeNotReady
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
for: 1h
labels:
severity: warning
annotations:
description: The Kubelet on {{ $labels.node }} has not checked in with the API,
or has set itself to NotReady, for more than an hour
summary: Node status is NotReady
- alert: K8SManyNodesNotReady
expr: count(kube_node_status_condition{condition="Ready",status="true"} == 0)
> 1 and (count(kube_node_status_condition{condition="Ready",status="true"} ==
0) / count(kube_node_status_condition{condition="Ready",status="true"})) > 0.2
for: 1m
labels:
severity: critical
annotations:
description: '{{ $value }}% of Kubernetes nodes are not ready'
- alert: K8SKubeletDown
expr: count(up{job="kubelet"} == 0) / count(up{job="kubelet"}) * 100 > 3
for: 1h
labels:
severity: warning
annotations:
description: Prometheus failed to scrape {{ $value }}% of kubelets.
- alert: K8SKubeletDown
expr: (absent(up{job="kubelet"} == 1) or count(up{job="kubelet"} == 0) / count(up{job="kubelet"}))
* 100 > 10
for: 1h
labels:
severity: critical
annotations:
description: Prometheus failed to scrape {{ $value }}% of kubelets, or all Kubelets
have disappeared from service discovery.
summary: Many Kubelets cannot be scraped
- alert: K8SKubeletTooManyPods
expr: kubelet_running_pod_count > 100
for: 10m
labels:
severity: warning
annotations:
description: Kubelet {{$labels.instance}} is running {{$value}} pods, close
to the limit of 110
summary: Kubelet is close to pod limit
kubernetes.rules.yaml: |
groups:
- name: kubernetes.rules
rules:
- record: pod_name:container_memory_usage_bytes:sum
expr: sum(container_memory_usage_bytes{container_name!="POD",pod_name!=""}) BY
(pod_name)
- record: pod_name:container_spec_cpu_shares:sum
expr: sum(container_spec_cpu_shares{container_name!="POD",pod_name!=""}) BY (pod_name)
- record: pod_name:container_cpu_usage:sum
expr: sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name!=""}[5m]))
BY (pod_name)
- record: pod_name:container_fs_usage_bytes:sum
expr: sum(container_fs_usage_bytes{container_name!="POD",pod_name!=""}) BY (pod_name)
- record: namespace:container_memory_usage_bytes:sum
expr: sum(container_memory_usage_bytes{container_name!=""}) BY (namespace)
- record: namespace:container_spec_cpu_shares:sum
expr: sum(container_spec_cpu_shares{container_name!=""}) BY (namespace)
- record: namespace:container_cpu_usage:sum
expr: sum(rate(container_cpu_usage_seconds_total{container_name!="POD"}[5m]))
BY (namespace)
- record: cluster:memory_usage:ratio
expr: sum(container_memory_usage_bytes{container_name!="POD",pod_name!=""}) BY
(cluster) / sum(machine_memory_bytes) BY (cluster)
- record: cluster:container_spec_cpu_shares:ratio
expr: sum(container_spec_cpu_shares{container_name!="POD",pod_name!=""}) / 1000
/ sum(machine_cpu_cores)
- record: cluster:container_cpu_usage:ratio
expr: sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name!=""}[5m]))
/ sum(machine_cpu_cores)
- record: apiserver_latency_seconds:quantile
expr: histogram_quantile(0.99, rate(apiserver_request_latencies_bucket[5m])) /
1e+06
labels:
quantile: "0.99"
- record: apiserver_latency:quantile_seconds
expr: histogram_quantile(0.9, rate(apiserver_request_latencies_bucket[5m])) /
1e+06
labels:
quantile: "0.9"
- record: apiserver_latency_seconds:quantile
expr: histogram_quantile(0.5, rate(apiserver_request_latencies_bucket[5m])) /
1e+06
labels:
quantile: "0.5"
- alert: APIServerLatencyHigh
expr: apiserver_latency_seconds:quantile{quantile="0.99",subresource!="log",verb!~"^(?:WATCH|WATCHLIST|PROXY|CONNECT)$"}
> 1
for: 10m
labels:
severity: warning
annotations:
description: the API server has a 99th percentile latency of {{ $value }} seconds
for {{$labels.verb}} {{$labels.resource}}
- alert: APIServerLatencyHigh
expr: apiserver_latency_seconds:quantile{quantile="0.99",subresource!="log",verb!~"^(?:WATCH|WATCHLIST|PROXY|CONNECT)$"}
> 4
for: 10m
labels:
severity: critical
annotations:
description: the API server has a 99th percentile latency of {{ $value }} seconds
for {{$labels.verb}} {{$labels.resource}}
- alert: APIServerErrorsHigh
expr: rate(apiserver_request_count{code=~"^(?:5..)$"}[5m]) / rate(apiserver_request_count[5m])
* 100 > 2
for: 10m
labels:
severity: warning
annotations:
description: API server returns errors for {{ $value }}% of requests
- alert: APIServerErrorsHigh
expr: rate(apiserver_request_count{code=~"^(?:5..)$"}[5m]) / rate(apiserver_request_count[5m])
* 100 > 5
for: 10m
labels:
severity: critical
annotations:
description: API server returns errors for {{ $value }}% of requests
- alert: K8SApiserverDown
expr: absent(up{job="apiserver"} == 1)
for: 20m
labels:
severity: critical
annotations:
description: No API servers are reachable or all have disappeared from service
discovery
- alert: K8sCertificateExpirationNotice
labels:
severity: warning
annotations:
description: Kubernetes API Certificate is expiring soon (less than 7 days)
expr: sum(apiserver_client_certificate_expiration_seconds_bucket{le="604800"}) > 0
- alert: K8sCertificateExpirationNotice
labels:
severity: critical
annotations:
description: Kubernetes API Certificate is expiring in less than 1 day
expr: sum(apiserver_client_certificate_expiration_seconds_bucket{le="86400"}) > 0
node.rules.yaml: |
groups:
- name: node.rules
rules:
- record: instance:node_cpu:rate:sum
expr: sum(rate(node_cpu{mode!="idle",mode!="iowait",mode!~"^(?:guest.*)$"}[3m]))
BY (instance)
- record: instance:node_filesystem_usage:sum
expr: sum((node_filesystem_size{mountpoint="/"} - node_filesystem_free{mountpoint="/"}))
BY (instance)
- record: instance:node_network_receive_bytes:rate:sum
expr: sum(rate(node_network_receive_bytes[3m])) BY (instance)
- record: instance:node_network_transmit_bytes:rate:sum
expr: sum(rate(node_network_transmit_bytes[3m])) BY (instance)
- record: instance:node_cpu:ratio
expr: sum(rate(node_cpu{mode!="idle"}[5m])) WITHOUT (cpu, mode) / ON(instance)
GROUP_LEFT() count(sum(node_cpu) BY (instance, cpu)) BY (instance)
- record: cluster:node_cpu:sum_rate5m
expr: sum(rate(node_cpu{mode!="idle"}[5m]))
- record: cluster:node_cpu:ratio
expr: cluster:node_cpu:rate5m / count(sum(node_cpu) BY (instance, cpu))
- alert: NodeExporterDown
expr: absent(up{job="node-exporter"} == 1)
for: 10m
labels:
severity: warning
annotations:
description: Prometheus could not scrape a node-exporter for more than 10m,
or node-exporters have disappeared from discovery
- alert: NodeDiskRunningFull
expr: predict_linear(node_filesystem_free[6h], 3600 * 24) < 0
for: 30m
labels:
severity: warning
annotations:
description: device {{$labels.device}} on node {{$labels.instance}} is running
full within the next 24 hours (mounted at {{$labels.mountpoint}})
- alert: NodeDiskRunningFull
expr: predict_linear(node_filesystem_free[30m], 3600 * 2) < 0
for: 10m
labels:
severity: critical
annotations:
description: device {{$labels.device}} on node {{$labels.instance}} is running
full within the next 2 hours (mounted at {{$labels.mountpoint}})
- alert: InactiveRAIDDisk
expr: node_md_disks - node_md_disks_active > 0
for: 10m
labels:
severity: warning
annotations:
description: '{{$value}} RAID disk(s) on node {{$labels.instance}} are inactive'
prometheus.rules.yaml: |
groups:
- name: prometheus.rules
rules:
- alert: PrometheusConfigReloadFailed
expr: prometheus_config_last_reload_successful == 0
for: 10m
labels:
severity: warning
annotations:
description: Reloading Prometheus' configuration has failed for {{$labels.namespace}}/{{$labels.pod}}
- alert: PrometheusNotificationQueueRunningFull
expr: predict_linear(prometheus_notifications_queue_length[5m], 60 * 30) > prometheus_notifications_queue_capacity
for: 10m
labels:
severity: warning
annotations:
description: Prometheus' alert notification queue is running full for {{$labels.namespace}}/{{
$labels.pod}}
- alert: PrometheusErrorSendingAlerts
expr: rate(prometheus_notifications_errors_total[5m]) / rate(prometheus_notifications_sent_total[5m])
> 0.01
for: 10m
labels:
severity: warning
annotations:
description: Errors while sending alerts from Prometheus {{$labels.namespace}}/{{
$labels.pod}} to Alertmanager {{$labels.Alertmanager}}
- alert: PrometheusErrorSendingAlerts
expr: rate(prometheus_notifications_errors_total[5m]) / rate(prometheus_notifications_sent_total[5m])
> 0.03
for: 10m
labels:
severity: critical
annotations:
description: Errors while sending alerts from Prometheus {{$labels.namespace}}/{{
$labels.pod}} to Alertmanager {{$labels.Alertmanager}}
- alert: PrometheusNotConnectedToAlertmanagers
expr: prometheus_notifications_alertmanagers_discovered < 1
for: 10m
labels:
severity: warning
annotations:
description: Prometheus {{ $labels.namespace }}/{{ $labels.pod}} is not connected
to any Alertmanagers
- alert: PrometheusTSDBReloadsFailing
expr: increase(prometheus_tsdb_reloads_failures_total[2h]) > 0
for: 12h
labels:
severity: warning
annotations:
description: '{{$labels.job}} at {{$labels.instance}} had {{$value | humanize}}
reload failures over the last four hours.'
summary: Prometheus has issues reloading data blocks from disk
- alert: PrometheusTSDBCompactionsFailing
expr: increase(prometheus_tsdb_compactions_failed_total[2h]) > 0
for: 12h
labels:
severity: warning
annotations:
description: '{{$labels.job}} at {{$labels.instance}} had {{$value | humanize}}
compaction failures over the last four hours.'
summary: Prometheus has issues compacting sample blocks
- alert: PrometheusTSDBWALCorruptions
expr: tsdb_wal_corruptions_total > 0
for: 4h
labels:
severity: warning
annotations:
description: '{{$labels.job}} at {{$labels.instance}} has a corrupted write-ahead
log (WAL).'
summary: Prometheus write-ahead log is corrupted
- alert: PrometheusNotIngestingSamples
expr: rate(prometheus_tsdb_head_samples_appended_total[5m]) <= 0
for: 10m
labels:
severity: warning
annotations:
description: "Prometheus {{ $labels.namespace }}/{{ $labels.pod}} isn't ingesting samples."
summary: "Prometheus isn't ingesting samples"
-18
View File
@@ -1,18 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: lens-prometheus
rules:
- apiGroups: [""]
resources:
- nodes
- nodes/proxy
- nodes/metrics
- services
- endpoints
- pods
- ingresses
- configmaps
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
@@ -1,12 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: lens-prometheus
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: lens-prometheus
subjects:
- kind: ServiceAccount
name: prometheus
namespace: lens-metrics
@@ -1,84 +0,0 @@
{{#if nodeExporter.enabled}}
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: lens-metrics
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
selector:
matchLabels:
name: node-exporter
phase: prod
template:
metadata:
labels:
name: node-exporter
phase: prod
annotations:
seccomp.security.alpha.kubernetes.io/pod: 'docker/default'
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
securityContext:
runAsNonRoot: true
runAsUser: 65534
hostPID: true
containers:
- name: node-exporter
image: docker.io/prom/node-exporter:v1.0.0-rc.0
args:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/host/root
- --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker|var/lib/containers/.+)($|/)
- --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$
ports:
- name: metrics
containerPort: 9100
resources:
requests:
cpu: 10m
memory: 24Mi
limits:
cpu: 200m
memory: 100Mi
volumeMounts:
- name: proc
mountPath: /host/proc
readOnly: true
- name: sys
mountPath: /host/sys
readOnly: true
- name: root
mountPath: /host/root
readOnly: true
tolerations:
- effect: NoSchedule
operator: Exists
volumes:
- name: proc
hostPath:
path: /proc
- name: sys
hostPath:
path: /sys
- name: root
hostPath:
path: /
{{/if}}
@@ -1,18 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: node-exporter
namespace: lens-metrics
annotations:
prometheus.io/scrape: 'true'
spec:
type: ClusterIP
clusterIP: None
selector:
name: node-exporter
phase: prod
ports:
- name: metrics
protocol: TCP
port: 80
targetPort: 9100
@@ -1,114 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: lens-kube-state-metrics
rules:
- apiGroups:
- ""
resources:
- configmaps
- secrets
- nodes
- pods
- services
- resourcequotas
- replicationcontrollers
- limitranges
- persistentvolumeclaims
- persistentvolumes
- namespaces
- endpoints
verbs:
- list
- watch
- apiGroups:
- extensions
resources:
- daemonsets
- deployments
- replicasets
- ingresses
verbs:
- list
- watch
- apiGroups:
- apps
resources:
- statefulsets
- daemonsets
- deployments
- replicasets
verbs:
- list
- watch
- apiGroups:
- batch
resources:
- cronjobs
- jobs
verbs:
- list
- watch
- apiGroups:
- autoscaling
resources:
- horizontalpodautoscalers
verbs:
- list
- watch
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- list
- watch
- apiGroups:
- certificates.k8s.io
resources:
- certificatesigningrequests
verbs:
- list
- watch
- apiGroups:
- storage.k8s.io
resources:
- storageclasses
- volumeattachments
verbs:
- list
- watch
- apiGroups:
- admissionregistration.k8s.io
resources:
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
verbs:
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- networkpolicies
verbs:
- list
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- list
- watch
@@ -1,5 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-state-metrics
namespace: lens-metrics
@@ -1,12 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: lens-kube-state-metrics
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: lens-kube-state-metrics
subjects:
- kind: ServiceAccount
name: kube-state-metrics
namespace: lens-metrics
@@ -1,59 +0,0 @@
{{#if kubeStateMetrics.enabled}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-state-metrics
namespace: lens-metrics
spec:
selector:
matchLabels:
name: kube-state-metrics
replicas: 1
template:
metadata:
labels:
name: kube-state-metrics
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
- key: kubernetes.io/arch
operator: In
values:
- amd64
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
- key: beta.kubernetes.io/arch
operator: In
values:
- amd64
serviceAccountName: kube-state-metrics
containers:
- name: kube-state-metrics
image: quay.io/coreos/kube-state-metrics:v1.9.5
ports:
- name: metrics
containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 5
resources:
requests:
cpu: 10m
memory: 150Mi
limits:
cpu: 200m
memory: 150Mi
{{/if}}
@@ -1,17 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: kube-state-metrics
namespace: lens-metrics
labels:
name: kube-state-metrics
annotations:
prometheus.io/scrape: 'true'
spec:
ports:
- name: metrics
port: 8080
targetPort: 8080
protocol: TCP
selector:
name: kube-state-metrics
-44
View File
@@ -1,44 +0,0 @@
import { Feature, FeatureStatus } from "../main/feature"
import {KubeConfig, RbacAuthorizationV1Api} from "@kubernetes/client-node"
import { Cluster } from "../main/cluster"
export class UserModeFeature extends Feature {
static id = 'user-mode'
name = UserModeFeature.id;
latestVersion = "v2.0.0"
async install(cluster: Cluster): Promise<void> {
return super.install(cluster)
}
async upgrade(cluster: Cluster): Promise<void> {
return;
}
async featureStatus(kc: KubeConfig): Promise<FeatureStatus> {
const client = kc.makeApiClient(RbacAuthorizationV1Api)
const status: FeatureStatus = {
currentVersion: null,
installed: false,
latestVersion: this.latestVersion,
canUpgrade: false, // Dunno yet
};
try {
await client.readClusterRoleBinding("lens-user")
status.installed = true;
status.currentVersion = this.latestVersion;
status.canUpgrade = false;
} catch {
// ignore error
}
return status;
}
async uninstall(cluster: Cluster): Promise<void> {
const rbacClient = cluster.getProxyKubeconfig().makeApiClient(RbacAuthorizationV1Api)
await rbacClient.deleteClusterRole("lens-user");
await rbacClient.deleteClusterRoleBinding("lens-user");
}
}
-11
View File
@@ -1,11 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: lens-user
rules:
- verbs:
- list
apiGroups:
- ''
resources:
- namespaces
@@ -1,12 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: lens-user
subjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: 'system:authenticated'
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: lens-user
+2
View File
@@ -49,6 +49,8 @@ export class ClusterManager {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
}
} else if (req.headers["x-cluster-id"]) {
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString())
} else {
const clusterId = getClusterIdFromHost(req.headers.host);
cluster = clusterStore.getById(clusterId)
+1 -23
View File
@@ -1,7 +1,6 @@
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store";
import type { FeatureStatusMap } from "./feature"
import { action, computed, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc";
@@ -10,7 +9,6 @@ import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from
import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager"
import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers"
import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
import request, { RequestPromiseOptions } from "request-promise-native"
import { apiResources } from "../common/rbac";
import logger from "./logger"
@@ -47,7 +45,6 @@ export interface ClusterState extends ClusterModel {
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
features: FeatureStatusMap;
}
export class Cluster implements ClusterModel {
@@ -78,7 +75,6 @@ export class Cluster implements ClusterModel {
@observable eventCount = 0;
@observable preferences: ClusterPreferences = {};
@observable metadata: ClusterMetadata = {};
@observable features: FeatureStatusMap = {};
@observable allowedNamespaces: string[] = [];
@observable allowedResources: string[] = [];
@@ -194,12 +190,7 @@ export class Cluster implements ClusterModel {
await this.whenInitialized;
await this.refreshConnectionStatus();
if (this.accessible) {
const [features, isAdmin] = await Promise.all([
getFeatures(this),
this.isClusterAdmin(),
]);
this.features = features;
this.isAdmin = isAdmin;
this.isAdmin = await this.isClusterAdmin();
await Promise.all([
this.refreshEvents(),
this.refreshAllowedResources(),
@@ -250,18 +241,6 @@ export class Cluster implements ClusterModel {
return this.kubeconfigManager.getPath()
}
async installFeature(name: string, config: any) {
return installFeature(name, this, config)
}
async upgradeFeature(name: string, config: any) {
return upgradeFeature(name, this, config)
}
async uninstallFeature(name: string) {
return uninstallFeature(name, this)
}
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
const apiUrl = this.kubeProxyUrl + path;
return request(apiUrl, {
@@ -400,7 +379,6 @@ export class Cluster implements ClusterModel {
accessible: this.accessible,
failureReason: this.failureReason,
isAdmin: this.isAdmin,
features: this.features,
eventCount: this.eventCount,
allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources,
-44
View File
@@ -1,44 +0,0 @@
import { KubeConfig } from "@kubernetes/client-node"
import logger from "./logger";
import { Cluster } from "./cluster";
import { Feature, FeatureStatusMap, FeatureMap } from "./feature"
import { MetricsFeature } from "../features/metrics"
import { UserModeFeature } from "../features/user-mode"
const ALL_FEATURES: Map<string, Feature> = new Map([
[MetricsFeature.id, new MetricsFeature(null)],
[UserModeFeature.id, new UserModeFeature(null)],
]);
export async function getFeatures(cluster: Cluster): Promise<FeatureStatusMap> {
const result: FeatureStatusMap = {};
logger.debug(`features for ${cluster.contextName}`);
for (const [key, feature] of ALL_FEATURES) {
logger.debug(`feature ${key}`);
logger.debug("getting feature status...");
const kc = new KubeConfig();
kc.loadFromFile(cluster.getProxyKubeconfigPath());
result[feature.name] = await feature.featureStatus(kc);
}
logger.debug(`getFeatures resolving with features: ${JSON.stringify(result)}`);
return result;
}
export async function installFeature(name: string, cluster: Cluster, config: any): Promise<void> {
// TODO Figure out how to handle config stuff
return ALL_FEATURES.get(name).install(cluster)
}
export async function upgradeFeature(name: string, cluster: Cluster, config: any): Promise<void> {
// TODO Figure out how to handle config stuff
return ALL_FEATURES.get(name).upgrade(cluster)
}
export async function uninstallFeature(name: string, cluster: Cluster): Promise<void> {
return ALL_FEATURES.get(name).uninstall(cluster)
}
-97
View File
@@ -1,97 +0,0 @@
import fs from "fs";
import path from "path"
import hb from "handlebars"
import { ResourceApplier } from "./resource-applier"
import { CoreV1Api, KubeConfig, Watch } from "@kubernetes/client-node"
import { Cluster } from "./cluster";
import logger from "./logger";
import { isDevelopment } from "../common/vars";
export type FeatureStatusMap = Record<string, FeatureStatus>
export type FeatureMap = Record<string, Feature>
export interface FeatureInstallRequest {
clusterId: string;
name: string;
config?: any;
}
export interface FeatureStatus {
currentVersion: string;
installed: boolean;
latestVersion: string;
canUpgrade: boolean;
}
export abstract class Feature {
public name: string;
public latestVersion: string;
abstract async upgrade(cluster: Cluster): Promise<void>;
abstract async uninstall(cluster: Cluster): Promise<void>;
abstract async featureStatus(kc: KubeConfig): Promise<FeatureStatus>;
constructor(public config: any) {
}
get folderPath() {
if (isDevelopment) {
return path.resolve(__static, "../src/features", this.name);
}
return path.resolve(__static, "../features", this.name);
}
async install(cluster: Cluster): Promise<void> {
const resources = this.renderTemplates();
try {
await new ResourceApplier(cluster).kubectlApplyAll(resources);
} catch (err) {
logger.error("Installing feature error", { err, cluster });
throw err;
}
}
protected async deleteNamespace(kc: KubeConfig, name: string) {
return new Promise(async (resolve, reject) => {
const client = kc.makeApiClient(CoreV1Api)
const result = await client.deleteNamespace("lens-metrics", 'false', undefined, undefined, undefined, "Foreground");
const nsVersion = result.body.metadata.resourceVersion;
const nsWatch = new Watch(kc);
const query: Record<string, string> = {
resourceVersion: nsVersion,
fieldSelector: "metadata.name=lens-metrics",
}
const req = await nsWatch.watch('/api/v1/namespaces', query,
(phase, obj) => {
if (phase === 'DELETED') {
logger.debug(`namespace ${name} finally gone`)
req.abort();
resolve()
}
},
(err?: any) => {
if (err) reject(err);
});
});
}
protected renderTemplates(): string[] {
const folderPath = this.folderPath;
const resources: string[] = [];
logger.info(`[FEATURE]: render templates from ${folderPath}`);
fs.readdirSync(folderPath).forEach(filename => {
const file = path.join(folderPath, filename);
const raw = fs.readFileSync(file);
if (filename.endsWith('.hb')) {
const template = hb.compile(raw.toString());
resources.push(template(this.config));
} else {
resources.push(raw.toString());
}
});
return resources;
}
}
@@ -3,11 +3,10 @@ import { KubeApi } from "../kube-api";
export class ClusterRoleBinding extends RoleBinding {
static kind = "ClusterRoleBinding"
static namespaced = false
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"
}
export const clusterRoleBindingApi = new KubeApi({
kind: ClusterRoleBinding.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings",
isNamespaced: false,
objectConstructor: ClusterRoleBinding,
});
@@ -5,11 +5,10 @@ import { KubeApi } from "../kube-api";
@autobind()
export class ClusterRole extends Role {
static kind = "ClusterRole"
static namespaced = false
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles"
}
export const clusterRoleApi = new KubeApi({
kind: ClusterRole.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/clusterroles",
isNamespaced: false,
objectConstructor: ClusterRole,
});
+4 -3
View File
@@ -3,6 +3,9 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class ClusterApi extends KubeApi<Cluster> {
static kind = "Cluster"
static namespaced = true
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
const nodes = nodeNames.join("|");
const opts = { category: "cluster", nodes: nodes }
@@ -49,6 +52,7 @@ export interface IClusterMetrics<T = IMetrics> {
export class Cluster extends KubeObject {
static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"
spec: {
clusterNetwork?: {
@@ -91,8 +95,5 @@ export class Cluster extends KubeObject {
}
export const clusterApi = new ClusterApi({
kind: Cluster.kind,
apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters",
isNamespaced: true,
objectConstructor: Cluster,
});
@@ -9,6 +9,8 @@ export interface IComponentStatusCondition {
export class ComponentStatus extends KubeObject {
static kind = "ComponentStatus"
static namespaced = false
static apiBase = "/api/v1/componentstatuses"
conditions: IComponentStatusCondition[]
@@ -18,8 +20,5 @@ export class ComponentStatus extends KubeObject {
}
export const componentStatusApi = new KubeApi({
kind: ComponentStatus.kind,
apiBase: "/api/v1/componentstatuses",
isNamespaced: false,
objectConstructor: ComponentStatus,
});
+2 -3
View File
@@ -6,6 +6,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class ConfigMap extends KubeObject {
static kind = "ConfigMap";
static namespaced = true;
static apiBase = "/api/v1/configmaps"
constructor(data: KubeJsonApiData) {
super(data);
@@ -22,8 +24,5 @@ export class ConfigMap extends KubeObject {
}
export const configMapApi = new KubeApi({
kind: ConfigMap.kind,
apiBase: "/api/v1/configmaps",
isNamespaced: true,
objectConstructor: ConfigMap,
});
+2 -3
View File
@@ -19,6 +19,8 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
export class CustomResourceDefinition extends KubeObject {
static kind = "CustomResourceDefinition";
static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"
spec: {
group: string;
@@ -145,9 +147,6 @@ export class CustomResourceDefinition extends KubeObject {
}
export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({
kind: CustomResourceDefinition.kind,
apiBase: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions",
isNamespaced: false,
objectConstructor: CustomResourceDefinition
});
+2 -3
View File
@@ -8,6 +8,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class CronJob extends KubeObject {
static kind = "CronJob"
static namespaced = true
static apiBase = "/apis/batch/v1beta1/cronjobs"
kind: string
apiVersion: string
@@ -88,8 +90,5 @@ export class CronJob extends KubeObject {
}
export const cronJobApi = new KubeApi({
kind: CronJob.kind,
apiBase: "/apis/batch/v1beta1/cronjobs",
isNamespaced: true,
objectConstructor: CronJob,
});
+2 -3
View File
@@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class DaemonSet extends WorkloadKubeObject {
static kind = "DaemonSet"
static namespaced = true
static apiBase = "/apis/apps/v1/daemonsets"
spec: {
selector: {
@@ -69,8 +71,5 @@ export class DaemonSet extends WorkloadKubeObject {
}
export const daemonSetApi = new KubeApi({
kind: DaemonSet.kind,
apiBase: "/apis/apps/v1/daemonsets",
isNamespaced: true,
objectConstructor: DaemonSet,
});
+2 -3
View File
@@ -28,6 +28,8 @@ export class DeploymentApi extends KubeApi<Deployment> {
@autobind()
export class Deployment extends WorkloadKubeObject {
static kind = "Deployment"
static namespaced = true
static apiBase = "/apis/apps/v1/deployments"
spec: {
replicas: number;
@@ -164,8 +166,5 @@ export class Deployment extends WorkloadKubeObject {
}
export const deploymentApi = new DeploymentApi({
kind: Deployment.kind,
apiBase: "/apis/apps/v1/deployments",
isNamespaced: true,
objectConstructor: Deployment,
});
+2 -3
View File
@@ -99,6 +99,8 @@ export class EndpointSubset implements IEndpointSubset {
@autobind()
export class Endpoint extends KubeObject {
static kind = "Endpoints"
static namespaced = true
static apiBase = "/api/v1/endpoints"
subsets: IEndpointSubset[]
@@ -118,8 +120,5 @@ export class Endpoint extends KubeObject {
}
export const endpointApi = new KubeApi({
kind: Endpoint.kind,
apiBase: "/api/v1/endpoints",
isNamespaced: true,
objectConstructor: Endpoint,
});
+2 -3
View File
@@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class KubeEvent extends KubeObject {
static kind = "Event"
static namespaced = true
static apiBase = "/api/v1/events"
involvedObject: {
kind: string;
@@ -52,8 +54,5 @@ export class KubeEvent extends KubeObject {
}
export const eventApi = new KubeApi({
kind: KubeEvent.kind,
apiBase: "/api/v1/events",
isNamespaced: true,
objectConstructor: KubeEvent,
})
+2 -3
View File
@@ -40,6 +40,8 @@ export interface IHpaMetric {
export class HorizontalPodAutoscaler extends KubeObject {
static kind = "HorizontalPodAutoscaler";
static namespaced = true;
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers"
spec: {
scaleTargetRef: {
@@ -133,8 +135,5 @@ export class HorizontalPodAutoscaler extends KubeObject {
}
export const hpaApi = new KubeApi({
kind: HorizontalPodAutoscaler.kind,
apiBase: "/apis/autoscaling/v2beta1/horizontalpodautoscalers",
isNamespaced: true,
objectConstructor: HorizontalPodAutoscaler,
});
+2 -3
View File
@@ -32,6 +32,8 @@ export interface ILoadBalancerIngress {
@autobind()
export class Ingress extends KubeObject {
static kind = "Ingress"
static namespaced = true
static apiBase = "/apis/extensions/v1beta1/ingresses"
spec: {
tls: {
@@ -110,8 +112,5 @@ export class Ingress extends KubeObject {
}
export const ingressApi = new IngressApi({
kind: Ingress.kind,
apiBase: "/apis/extensions/v1beta1/ingresses",
isNamespaced: true,
objectConstructor: Ingress,
});
+2 -3
View File
@@ -8,6 +8,8 @@ import { JsonApiParams } from "../json-api";
@autobind()
export class Job extends WorkloadKubeObject {
static kind = "Job"
static namespaced = true
static apiBase = "/apis/batch/v1/jobs"
spec: {
parallelism?: number;
@@ -102,8 +104,5 @@ export class Job extends WorkloadKubeObject {
}
export const jobApi = new KubeApi({
kind: Job.kind,
apiBase: "/apis/batch/v1/jobs",
isNamespaced: true,
objectConstructor: Job,
});
+2 -3
View File
@@ -10,6 +10,8 @@ export enum NamespaceStatus {
@autobind()
export class Namespace extends KubeObject {
static kind = "Namespace";
static namespaced = false;
static apiBase = "/api/v1/namespaces";
status?: {
phase: string;
@@ -21,8 +23,5 @@ export class Namespace extends KubeObject {
}
export const namespacesApi = new KubeApi({
kind: Namespace.kind,
apiBase: "/api/v1/namespaces",
isNamespaced: false,
objectConstructor: Namespace,
});
@@ -38,6 +38,8 @@ export interface IPolicyEgress {
@autobind()
export class NetworkPolicy extends KubeObject {
static kind = "NetworkPolicy"
static namespaced = true
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies"
spec: {
podSelector: {
@@ -65,8 +67,5 @@ export class NetworkPolicy extends KubeObject {
}
export const networkPolicyApi = new KubeApi({
kind: NetworkPolicy.kind,
apiBase: "/apis/networking.k8s.io/v1/networkpolicies",
isNamespaced: true,
objectConstructor: NetworkPolicy,
});
+2 -3
View File
@@ -31,6 +31,8 @@ export interface INodeMetrics<T = IMetrics> {
@autobind()
export class Node extends KubeObject {
static kind = "Node"
static namespaced = false
static apiBase = "/api/v1/nodes"
spec: {
podCIDR: string;
@@ -156,8 +158,5 @@ export class Node extends KubeObject {
}
export const nodesApi = new NodesApi({
kind: Node.kind,
apiBase: "/api/v1/nodes",
isNamespaced: false,
objectConstructor: Node,
});
@@ -24,6 +24,8 @@ export interface IPvcMetrics<T = IMetrics> {
@autobind()
export class PersistentVolumeClaim extends KubeObject {
static kind = "PersistentVolumeClaim"
static namespaced = true
static apiBase = "/api/v1/persistentvolumeclaims"
spec: {
accessModes: string[];
@@ -81,8 +83,5 @@ export class PersistentVolumeClaim extends KubeObject {
}
export const pvcApi = new PersistentVolumeClaimsApi({
kind: PersistentVolumeClaim.kind,
apiBase: "/api/v1/persistentvolumeclaims",
isNamespaced: true,
objectConstructor: PersistentVolumeClaim,
});
@@ -6,6 +6,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class PersistentVolume extends KubeObject {
static kind = "PersistentVolume"
static namespaced = false
static apiBase = "/api/v1/persistentvolumes"
spec: {
capacity: {
@@ -64,8 +66,5 @@ export class PersistentVolume extends KubeObject {
}
export const persistentVolumeApi = new KubeApi({
kind: PersistentVolume.kind,
apiBase: "/api/v1/persistentvolumes",
isNamespaced: false,
objectConstructor: PersistentVolume,
});
@@ -2,6 +2,10 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class PodMetrics extends KubeObject {
static kind = "Pod"
static namespaced = true
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods"
timestamp: string
window: string
containers: {
@@ -14,8 +18,5 @@ export class PodMetrics extends KubeObject {
}
export const podMetricsApi = new KubeApi({
kind: PodMetrics.kind,
apiBase: "/apis/metrics.k8s.io/v1beta1/pods",
isNamespaced: true,
objectConstructor: PodMetrics,
});
@@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class PodDisruptionBudget extends KubeObject {
static kind = "PodDisruptionBudget";
static namespaced = true;
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
spec: {
minAvailable: string;
@@ -42,8 +44,5 @@ export class PodDisruptionBudget extends KubeObject {
}
export const pdbApi = new KubeApi({
kind: PodDisruptionBudget.kind,
apiBase: "/apis/policy/v1beta1/poddisruptionbudgets",
isNamespaced: true,
objectConstructor: PodDisruptionBudget,
});
+2 -3
View File
@@ -163,6 +163,8 @@ export interface IPodContainerStatus {
@autobind()
export class Pod extends WorkloadKubeObject {
static kind = "Pod"
static namespaced = true
static apiBase = "/api/v1/pods"
spec: {
volumes?: {
@@ -419,8 +421,5 @@ export class Pod extends WorkloadKubeObject {
}
export const podsApi = new PodsApi({
kind: Pod.kind,
apiBase: "/api/v1/pods",
isNamespaced: true,
objectConstructor: Pod,
});
@@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class PodSecurityPolicy extends KubeObject {
static kind = "PodSecurityPolicy"
static namespaced = false
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies"
spec: {
allowPrivilegeEscalation?: boolean;
@@ -87,8 +89,5 @@ export class PodSecurityPolicy extends KubeObject {
}
export const pspApi = new KubeApi({
kind: PodSecurityPolicy.kind,
apiBase: "/apis/policy/v1beta1/podsecuritypolicies",
isNamespaced: false,
objectConstructor: PodSecurityPolicy,
});
@@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class ReplicaSet extends WorkloadKubeObject {
static kind = "ReplicaSet"
static namespaced = true
static apiBase = "/apis/apps/v1/replicasets"
spec: {
replicas?: number;
@@ -51,8 +53,5 @@ export class ReplicaSet extends WorkloadKubeObject {
}
export const replicaSetApi = new KubeApi({
kind: ReplicaSet.kind,
apiBase: "/apis/apps/v1/replicasets",
isNamespaced: true,
objectConstructor: ReplicaSet,
});
@@ -32,6 +32,8 @@ export interface IResourceQuotaValues {
export class ResourceQuota extends KubeObject {
static kind = "ResourceQuota"
static namespaced = true
static apiBase = "/api/v1/resourcequotas"
constructor(data: KubeJsonApiData) {
super(data);
@@ -61,8 +63,5 @@ export class ResourceQuota extends KubeObject {
}
export const resourceQuotaApi = new KubeApi({
kind: ResourceQuota.kind,
apiBase: "/api/v1/resourcequotas",
isNamespaced: true,
objectConstructor: ResourceQuota,
});
@@ -12,6 +12,8 @@ export interface IRoleBindingSubject {
@autobind()
export class RoleBinding extends KubeObject {
static kind = "RoleBinding"
static namespaced = true
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings"
subjects?: IRoleBindingSubject[]
roleRef: {
@@ -30,8 +32,5 @@ export class RoleBinding extends KubeObject {
}
export const roleBindingApi = new KubeApi({
kind: RoleBinding.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/rolebindings",
isNamespaced: true,
objectConstructor: RoleBinding,
});
+2 -3
View File
@@ -3,6 +3,8 @@ import { KubeApi } from "../kube-api";
export class Role extends KubeObject {
static kind = "Role"
static namespaced = true
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles"
rules: {
verbs: string[];
@@ -17,8 +19,5 @@ export class Role extends KubeObject {
}
export const roleApi = new KubeApi({
kind: Role.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/roles",
isNamespaced: true,
objectConstructor: Role,
});
+2 -3
View File
@@ -22,6 +22,8 @@ export interface ISecretRef {
@autobind()
export class Secret extends KubeObject {
static kind = "Secret"
static namespaced = true
static apiBase = "/api/v1/secrets"
type: SecretType;
data: {
@@ -44,8 +46,5 @@ export class Secret extends KubeObject {
}
export const secretsApi = new KubeApi({
kind: Secret.kind,
apiBase: "/api/v1/secrets",
isNamespaced: true,
objectConstructor: Secret,
});
@@ -22,6 +22,8 @@ export interface ISelfSubjectReviewRule {
export class SelfSubjectRulesReview extends KubeObject {
static kind = "SelfSubjectRulesReview"
static namespaced = false
static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"
spec: {
// todo: add more types from api docs
@@ -61,8 +63,5 @@ export class SelfSubjectRulesReview extends KubeObject {
}
export const selfSubjectRulesReviewApi = new SelfSubjectRulesReviewApi({
kind: SelfSubjectRulesReview.kind,
apiBase: "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews",
isNamespaced: false,
objectConstructor: SelfSubjectRulesReview,
});
@@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class ServiceAccount extends KubeObject {
static kind = "ServiceAccount";
static namespaced = true;
static apiBase = "/api/v1/serviceaccounts"
secrets?: {
name: string;
@@ -23,8 +25,5 @@ export class ServiceAccount extends KubeObject {
}
export const serviceAccountsApi = new KubeApi<ServiceAccount>({
kind: ServiceAccount.kind,
apiBase: "/api/v1/serviceaccounts",
isNamespaced: true,
objectConstructor: ServiceAccount,
});
+2 -3
View File
@@ -32,6 +32,8 @@ export class ServicePort implements IServicePort {
@autobind()
export class Service extends KubeObject {
static kind = "Service"
static namespaced = true
static apiBase = "/api/v1/services"
spec: {
type: string;
@@ -93,8 +95,5 @@ export class Service extends KubeObject {
}
export const serviceApi = new KubeApi({
kind: Service.kind,
apiBase: "/api/v1/services",
isNamespaced: true,
objectConstructor: Service,
});
@@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class StatefulSet extends WorkloadKubeObject {
static kind = "StatefulSet"
static namespaced = true
static apiBase = "/apis/apps/v1/statefulsets"
spec: {
serviceName: string;
@@ -77,8 +79,5 @@ export class StatefulSet extends WorkloadKubeObject {
}
export const statefulSetApi = new KubeApi({
kind: StatefulSet.kind,
apiBase: "/apis/apps/v1/statefulsets",
isNamespaced: true,
objectConstructor: StatefulSet,
});
@@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind()
export class StorageClass extends KubeObject {
static kind = "StorageClass"
static namespaced = false
static apiBase = "/apis/storage.k8s.io/v1/storageclasses"
provisioner: string; // e.g. "storage.k8s.io/v1"
mountOptions?: string[];
@@ -32,8 +34,5 @@ export class StorageClass extends KubeObject {
}
export const storageClassApi = new KubeApi({
kind: StorageClass.kind,
apiBase: "/apis/storage.k8s.io/v1/storageclasses",
isNamespaced: false,
objectConstructor: StorageClass,
});
+31 -7
View File
@@ -8,13 +8,15 @@ import { apiKube } from "./index";
import { kubeWatchApi } from "./kube-watch-api";
import { apiManager } from "./api-manager";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import { apiKubePrefix, isDevelopment } from "../../common/vars";
import * as URL from "url"
export interface IKubeApiOptions<T extends KubeObject> {
kind: string; // resource type within api-group, e.g. "Namespace"
apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
isNamespaced: boolean;
apiBase?: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
objectConstructor?: IKubeObjectConstructor<T>;
request?: KubeJsonApi;
isNamespaced?: boolean;
kind?: string;
}
export interface IKubeApiQueryParams {
@@ -25,6 +27,25 @@ export interface IKubeApiQueryParams {
continue?: string; // might be used with ?limit from second request
}
export interface IKubeApiCluster {
id: string;
}
export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeClass: IKubeObjectConstructor<T>): KubeApi<T> {
const request = new KubeJsonApi({
apiBase: apiKubePrefix,
debug: isDevelopment,
}, {
headers: {
"X-Cluster-ID": cluster.id
}
});
return new KubeApi({
objectConstructor: kubeClass,
request: request
})
}
export class KubeApi<T extends KubeObject = any> {
static parseApi = parseKubeApi;
@@ -48,11 +69,14 @@ export class KubeApi<T extends KubeObject = any> {
constructor(protected options: IKubeApiOptions<T>) {
const {
kind,
isNamespaced = false,
objectConstructor = KubeObject as IKubeObjectConstructor,
request = apiKube
request = apiKube,
kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced
} = options || {};
if (!options.apiBase) {
options.apiBase = objectConstructor.apiBase
}
const { apiBase, apiPrefix, apiGroup, apiVersion, apiVersionWithGroup, resource } = KubeApi.parseApi(options.apiBase);
this.kind = kind;
@@ -174,4 +198,4 @@ export class KubeApi<T extends KubeObject = any> {
}
}
export * from "./kube-api-parse"
export * from "./kube-api-parse"
+4 -1
View File
@@ -10,6 +10,8 @@ import { resourceApplierApi } from "./endpoints/resource-applier.api";
export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & {
kind?: string;
namespaced?: boolean;
apiBase?: string;
};
export interface IKubeObjectMetadata {
@@ -43,6 +45,7 @@ export type IKubeMetaField = keyof IKubeObjectMetadata;
@autobind()
export class KubeObject implements ItemObject {
static readonly kind: string;
static readonly namespaced: boolean;
static create(data: any) {
return new KubeObject(data);
@@ -157,4 +160,4 @@ export class KubeObject implements ItemObject {
delete(params?: JsonApiParams) {
return apiKube.del(this.selfLink, params);
}
}
}
@@ -1,72 +1,87 @@
import React from "react";
import { observable, reaction, comparer } from "mobx";
import { observer, disposeOnUnmount } from "mobx-react";
import { clusterIpc } from "../../../../common/cluster-ipc";
import { Cluster } from "../../../../main/cluster";
import { Button } from "../../button";
import { Notifications } from "../../notifications";
import { Spinner } from "../../spinner";
import { ClusterFeature } from "../../../../extensions/cluster-feature";
import { interval } from "../../../utils";
interface Props {
cluster: Cluster
feature: string
feature: ClusterFeature
}
@observer
export class InstallFeature extends React.Component<Props> {
@observable loading = false;
@observable message = "";
componentDidMount() {
const feature = this.props.feature
const cluster = this.props.cluster
const statusUpdate = interval(20, () => {
feature.updateStatus(cluster)
})
statusUpdate.start(true)
disposeOnUnmount(this, () => {
statusUpdate.stop()
})
disposeOnUnmount(this,
reaction(() => this.props.cluster.features[this.props.feature], () => {
reaction(() => feature.status.installed, () => {
this.loading = false;
this.message = ""
}, { equals: comparer.structural })
);
}
getActionButtons() {
const { cluster, feature } = this.props;
const features = cluster.features[feature];
const disabled = !cluster.isAdmin || this.loading;
const loadingIcon = this.loading ? <Spinner/> : null;
if (!features) return null;
return (
<div className="flex gaps align-center">
{features.canUpgrade &&
{feature.status.canUpgrade &&
<Button
primary
disabled={disabled}
onClick={this.runAction(() =>
clusterIpc.upgradeFeature.invokeFromRenderer(cluster.id, feature))
}
feature.upgrade(cluster)
)}
>
Upgrade
</Button>
}
{features.installed &&
{feature.status.installed &&
<Button
accent
disabled={disabled}
onClick={this.runAction(() =>
clusterIpc.uninstallFeature.invokeFromRenderer(cluster.id, feature))
}
onClick={this.runAction(async () => {
this.message = "Uninstalling feature ..."
feature.uninstall(cluster)
})}
>
Uninstall
</Button>
}
{!features.installed && !features.canUpgrade &&
{!feature.status.installed && !feature.status.canUpgrade &&
<Button
primary
disabled={disabled}
onClick={this.runAction(() =>
clusterIpc.installFeature.invokeFromRenderer(cluster.id, feature))
}
onClick={this.runAction(async () =>{
this.message = "Installing feature ..."
feature.install(cluster)
})}
>
Install
</Button>
}
{loadingIcon}
{!cluster.isAdmin && <span className='admin-note'>Actions can only be performed by admins.</span>}
{cluster.isAdmin && this.loading && this.message !== "" && <span className='admin-note'>{this.message}</span>}
</div>
);
}
@@ -90,4 +105,4 @@ export class InstallFeature extends React.Component<Props> {
</>
);
}
}
}
@@ -2,8 +2,7 @@ import React from "react";
import { Cluster } from "../../../main/cluster";
import { InstallFeature } from "./components/install-feature";
import { SubTitle } from "../layout/sub-title";
import { MetricsFeature } from "../../../features/metrics";
import { UserModeFeature } from "../../../features/user-mode";
import { clusterFeatureRegistry } from "../../../extensions/registries/cluster-feature-registry";
interface Props {
cluster: Cluster;
@@ -12,31 +11,20 @@ interface Props {
export class Features extends React.Component<Props> {
render() {
const { cluster } = this.props;
return (
<div>
<h2>Features</h2>
<InstallFeature cluster={cluster} feature={MetricsFeature.id}>
<>
<SubTitle title="Metrics"/>
<p>
Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests{" "}
<a href="https://github.com/lensapp/lens/tree/master/src/features/metrics" target="_blank">here</a>.
</p>
</>
</InstallFeature>
<InstallFeature cluster={cluster} feature={UserModeFeature.id}>
<>
<SubTitle title="User Mode"/>
<p>
User Mode feature enables non-admin users to see namespaces they have access to.{" "}
This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
</p>
</>
</InstallFeature>
{ clusterFeatureRegistry.getItems().map((f) => {
return (
<InstallFeature cluster={cluster} feature={f.feature}>
<>
<SubTitle title={f.title}/>
<p><f.components.Description /></p>
</>
</InstallFeature>
)
})}
</div>
);
}
}
}