project restructuring and initial refactoring

This commit is contained in:
Roman committed 2020-06-09 15:14:40 +03:00
1 parent 5af12bdbbd
commit aa0eca2dc4
807 files changed
+11848 -22947

No files matched your search

View File
Whitespace-only changes.
-9
View File
@@ -1,9 +0,0 @@
const userStore = {
getPreferences: jest.fn(() => {
return {
downloadMirror: "default"
}
})
}
export { userStore };
-14
View File
@@ -1,14 +0,0 @@
import { app, remote } from "electron"
/**
*
* @returns app version correctly regardless of dev/prod mode and main/renderer differences
*/
export function getAppVersion(): string {
// app is undefined when running in renderer
let version = (app || remote.app).getVersion()
if(process.env.NODE_ENV === 'development') {
version = require("../../package.json").version
}
return version;
}
-113
View File
@@ -1,113 +0,0 @@
import * as ElectronStore from "electron-store"
import { Cluster, ClusterBaseInfo } from "../main/cluster";
import { getAppVersion } from "./app-utils"
import * as version200Beta2 from "./migrations/cluster-store/2.0.0-beta.2"
import * as version241 from "./migrations/cluster-store/2.4.1"
import * as version260Beta2 from "./migrations/cluster-store/2.6.0-beta.2"
import * as version260Beta3 from "./migrations/cluster-store/2.6.0-beta.3"
import * as version270Beta0 from "./migrations/cluster-store/2.7.0-beta.0"
import * as version270Beta1 from "./migrations/cluster-store/2.7.0-beta.1"
export class ClusterStore {
private static instance: ClusterStore;
public store: ElectronStore;
private constructor() {
this.store = new ElectronStore({
name: "lens-cluster-store",
projectVersion: getAppVersion(),
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: {
"2.0.0-beta.2": version200Beta2.migration,
"2.4.1": version241.migration,
"2.6.0-beta.2": version260Beta2.migration,
"2.6.0-beta.3": version260Beta3.migration,
"2.7.0-beta.0": version270Beta0.migration,
"2.7.0-beta.1": version270Beta1.migration
}
})
}
public getAllClusterObjects(): Array<Cluster> {
return this.store.get("clusters", []).map((clusterInfo: ClusterBaseInfo) => {
return new Cluster(clusterInfo)
})
}
public getAllClusters(): Array<ClusterBaseInfo> {
return this.store.get("clusters", [])
}
public removeCluster(id: string): void {
this.store.delete(id);
const clusterBaseInfos = this.getAllClusters()
const index = clusterBaseInfos.findIndex((cbi) => cbi.id === id)
if (index !== -1) {
clusterBaseInfos.splice(index, 1)
this.store.set("clusters", clusterBaseInfos)
}
}
public removeClustersByWorkspace(workspace: string) {
this.getAllClusters().forEach((cluster) => {
if (cluster.workspace === workspace) {
this.removeCluster(cluster.id)
}
})
}
public getCluster(id: string): Cluster {
const cluster = this.getAllClusterObjects().find((cluster) => cluster.id === id)
if (cluster) { return cluster}
return null
}
public storeCluster(cluster: ClusterBaseInfo) {
const clusters = this.getAllClusters();
const index = clusters.findIndex((cl) => cl.id === cluster.id)
const storable = {
id: cluster.id,
kubeConfig: cluster.kubeConfig,
preferences: cluster.preferences,
workspace: cluster.workspace
}
if (index === -1) {
clusters.push(storable)
} else {
clusters[index] = storable
}
this.store.set("clusters", clusters)
}
public storeClusters(clusters: ClusterBaseInfo[]) {
clusters.forEach((cluster: ClusterBaseInfo) => {
this.removeCluster(cluster.id)
this.storeCluster(cluster)
})
}
public reloadCluster(cluster: ClusterBaseInfo): void {
const storedCluster = this.getCluster(cluster.id);
if (storedCluster) {
cluster.kubeConfig = storedCluster.kubeConfig
cluster.preferences = storedCluster.preferences
cluster.workspace = storedCluster.workspace
}
}
static getInstance(): ClusterStore {
if(!ClusterStore.instance) {
ClusterStore.instance = new ClusterStore();
}
return ClusterStore.instance;
}
static resetInstance() {
ClusterStore.instance = null
}
}
const clusterStore: ClusterStore = ClusterStore.getInstance();
export { clusterStore };
@@ -1,13 +0,0 @@
/* Early store format had the kubeconfig directly under context name, this moves
it under the kubeConfig key */
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.0.0-beta.2");
for (const value of store) {
const contextName = value[0];
// Looping all the keys gives out the store internal stuff too...
if(contextName === "__internal__" || value[1].hasOwnProperty('kubeConfig')) continue;
store.set(contextName, { kubeConfig: value[1] });
}
}
@@ -1,11 +0,0 @@
// Cleans up a store that had the state related data stored
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.4.1");
for (const value of store) {
const contextName = value[0];
if(contextName === "__internal__") continue;
const cluster = value[1];
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
}
}
@@ -1,15 +0,0 @@
// Move cluster icon from root to preferences
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.2");
for (const value of store) {
const clusterKey = value[0];
if(clusterKey === "__internal__") continue
const cluster = value[1];
if(!cluster.preferences) cluster.preferences = {};
if(cluster.icon) {
cluster.preferences.icon = cluster.icon;
delete(cluster["icon"]);
}
store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences });
}
}
@@ -1,35 +0,0 @@
import * as yaml from "js-yaml"
// Convert access token and expiry from arrays into strings
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.3");
for (const value of store) {
const clusterKey = value[0];
if(clusterKey === "__internal__") continue
const cluster = value[1];
if(!cluster.kubeConfig) continue
const kubeConfig = yaml.safeLoad(cluster.kubeConfig)
if(!kubeConfig.hasOwnProperty('users')) continue
const userObj = kubeConfig.users[0]
if (userObj) {
const user = userObj.user
if (user["auth-provider"] && user["auth-provider"].config) {
const authConfig = user["auth-provider"].config
if (authConfig["access-token"]) {
authConfig["access-token"] = `${authConfig["access-token"]}`
}
if (authConfig.expiry) {
authConfig.expiry = `${authConfig.expiry}`
}
console.log(authConfig)
user["auth-provider"].config = authConfig
kubeConfig.users = [{
name: userObj.name,
user: user
}]
cluster.kubeConfig = yaml.safeDump(kubeConfig)
store.set(clusterKey, cluster)
}
}
}
}
@@ -1,11 +0,0 @@
// Add existing clusters to "default" workspace
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.0");
for (const value of store) {
const clusterKey = value[0];
if(clusterKey === "__internal__") continue
const cluster = value[1];
cluster.workspace = "default"
store.set(clusterKey, cluster)
}
}
@@ -1,22 +0,0 @@
// add id for clusters and store them to array
import { v4 as uuid } from "uuid"
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.1");
const clusters: any[] = []
for (const value of store) {
const clusterKey = value[0];
if(clusterKey === "__internal__") continue
if(clusterKey === "clusters") continue
const cluster = value[1];
cluster.id = uuid()
if (!cluster.preferences.clusterName) {
cluster.preferences.clusterName = clusterKey
}
clusters.push(cluster)
store.delete(clusterKey)
}
if (clusters.length > 0) {
store.set("clusters", clusters)
}
}
@@ -1,4 +0,0 @@
// Add / reset "lastSeenAppVersion"
export function migration(store: any) {
store.set("lastSeenAppVersion", "0.0.0");
}
-11
View File
@@ -1,11 +0,0 @@
import { PrometheusLens } from "../main/prometheus/lens";
import { PrometheusHelm } from "../main/prometheus/helm";
import { PrometheusOperator } from "../main/prometheus/operator";
import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry";
[PrometheusLens, PrometheusHelm, PrometheusOperator].forEach(providerClass => {
const provider = new providerClass()
PrometheusProviderRegistry.registerProvider(provider.id, provider)
});
export const prometheusProviders = PrometheusProviderRegistry.getProviders()
-12
View File
@@ -1,12 +0,0 @@
import * as request from "request"
import { userStore } from "../common/user-store"
export function globalRequestOpts(requestOpts: request.Options ) {
const userPrefs = userStore.getPreferences()
if (userPrefs.httpsProxy) {
requestOpts.proxy = userPrefs.httpsProxy
}
requestOpts.rejectUnauthorized = !userPrefs.allowUntrustedCAs;
return requestOpts
}
-6
View File
@@ -1,6 +0,0 @@
import * as winca from "win-ca/api"
import "mac-ca"
if (process.platform === "win32") {
winca.inject("+") // see: https://github.com/ukoloff/win-ca#caveats
}
-42
View File
@@ -1,42 +0,0 @@
import { machineIdSync } from 'node-machine-id'
import { userStore } from "../common/user-store"
import * as ua from "universal-analytics"
const GA_ID = "UA-159377374-1"
export class Tracker {
protected visitor: ua.Visitor
protected machineId: string = null;
protected ip: string = null;
protected appVersion: string;
protected locale: string;
protected electronUA: string;
constructor(app: Electron.App) {
try {
this.visitor = ua(GA_ID, machineIdSync(), {strictCidFormat: false})
} catch (error) {
this.visitor = ua(GA_ID)
}
this.visitor.set("dl", "https://lensapptelemetry.lakendlabs.com")
}
public async event(eventCategory: string, eventAction: string) {
return new Promise(async (resolve, reject) => {
if (!this.telemetryAllowed()) {
resolve()
return
}
this.visitor.event({
ec: eventCategory,
ea: eventAction
}).send()
resolve()
})
}
protected telemetryAllowed() {
const userPrefs = userStore.getPreferences()
return !!userPrefs.allowTelemetry
}
}
-84
View File
@@ -1,84 +0,0 @@
import * as ElectronStore from "electron-store"
import * as appUtil from "./app-utils"
import * as version210Beta4 from "./migrations/user-store/2.1.0-beta.4"
export interface User {
id?: string;
}
export interface UserPreferences {
httpsProxy?: string;
colorTheme?: string;
allowUntrustedCAs?: boolean;
allowTelemetry?: boolean;
downloadMirror?: string;
}
export class UserStore {
private static instance: UserStore;
public store: ElectronStore;
private constructor() {
this.store = new ElectronStore({
projectVersion: appUtil.getAppVersion(),
migrations: {
"2.1.0-beta.4": version210Beta4.migration,
}
});
}
public lastSeenAppVersion() {
return this.store.get('lastSeenAppVersion', "0.0.0")
}
public setLastSeenAppVersion(version: string) {
this.store.set('lastSeenAppVersion', version)
}
public getSeenContexts(): Array<string> {
return this.store.get("seenContexts", [])
}
public storeSeenContext(newContexts: string[]) {
const seenContexts = this.getSeenContexts().concat(newContexts)
// store unique contexts by casting array to set first
const newContextSet = new Set(seenContexts)
const allContexts = [...newContextSet]
this.store.set("seenContexts", allContexts)
return allContexts
}
public setPreferences(preferences: UserPreferences) {
this.store.set('preferences', preferences)
}
public getPreferences(): UserPreferences {
const prefs = this.store.get("preferences", {})
if (!prefs.colorTheme) {
prefs.colorTheme = "dark"
}
if (!prefs.downloadMirror) {
prefs.downloadMirror = "default"
}
if (prefs.allowTelemetry === undefined) {
prefs.allowTelemetry = true
}
return prefs
}
static getInstance(): UserStore {
if(!UserStore.instance) {
UserStore.instance = new UserStore();
}
return UserStore.instance;
}
static resetInstance() {
UserStore.instance = null
}
}
const userStore: UserStore = UserStore.getInstance();
export { userStore };
-78
View File
@@ -1,78 +0,0 @@
import * as ElectronStore from "electron-store"
import { clusterStore } from "./cluster-store"
export interface WorkspaceData {
id: string;
name: string;
description?: string;
}
export class Workspace implements WorkspaceData {
public id: string
public name: string
public description?: string
public constructor(data: WorkspaceData) {
Object.assign(this, data)
}
}
export class WorkspaceStore {
public static defaultId = "default"
private static instance: WorkspaceStore;
public store: ElectronStore;
private constructor() {
this.store = new ElectronStore({
name: "lens-workspace-store"
})
}
public storeWorkspace(workspace: WorkspaceData) {
const workspaces = this.getAllWorkspaces()
const index = workspaces.findIndex((w) => w.id === workspace.id)
if (index !== -1) {
workspaces[index] = workspace
} else {
workspaces.push(workspace)
}
this.store.set("workspaces", workspaces)
}
public removeWorkspace(workspace: Workspace) {
if (workspace.id === WorkspaceStore.defaultId) {
throw new Error("Cannot remove default workspace")
}
const workspaces = this.getAllWorkspaces()
const index = workspaces.findIndex((w) => w.id === workspace.id)
if (index !== -1) {
clusterStore.removeClustersByWorkspace(workspace.id)
workspaces.splice(index, 1)
this.store.set("workspaces", workspaces)
}
}
public getAllWorkspaces(): Array<Workspace> {
const workspacesData: WorkspaceData[] = this.store.get("workspaces", [])
return workspacesData.map((wsd) => new Workspace(wsd))
}
static getInstance(): WorkspaceStore {
if (!WorkspaceStore.instance) {
WorkspaceStore.instance = new WorkspaceStore()
}
return WorkspaceStore.instance
}
}
const workspaceStore: WorkspaceStore = WorkspaceStore.getInstance()
if (!workspaceStore.getAllWorkspaces().find( ws => ws.id === WorkspaceStore.defaultId)) {
workspaceStore.storeWorkspace({
id: WorkspaceStore.defaultId,
name: "default"
})
}
export { workspaceStore }
-108
View File
@@ -1,108 +0,0 @@
import { Feature, FeatureStatus } from "../main/feature"
import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node"
import * as 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 {
name = 'metrics';
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<boolean> {
// Check if there are storageclasses
const storageClient = cluster.contextHandler.kc.makeApiClient(k8s.StorageV1Api)
const scs = await storageClient.listStorageClass();
scs.body.items.forEach(sc => {
if(sc.metadata.annotations &&
(sc.metadata.annotations['storageclass.kubernetes.io/is-default-class'] === 'true' || sc.metadata.annotations['storageclass.beta.kubernetes.io/is-default-class'] === 'true')) {
this.config.persistence.enabled = true;
}
});
return super.install(cluster)
}
async upgrade(cluster: Cluster): Promise<boolean> {
return this.install(cluster)
}
async featureStatus(kc: KubeConfig): Promise<FeatureStatus> {
return new Promise<FeatureStatus>( async (resolve, reject) => {
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);
resolve(status)
} catch(error) {
resolve(status)
}
});
}
async uninstall(cluster: Cluster): Promise<boolean> {
return new Promise<boolean>(async (resolve, reject) => {
const rbacClient = cluster.contextHandler.kc.makeApiClient(RbacAuthorizationV1Api)
try {
await this.deleteNamespace(cluster.contextHandler.kc, "lens-metrics")
await rbacClient.deleteClusterRole("lens-prometheus");
await rbacClient.deleteClusterRoleBinding("lens-prometheus");
resolve(true);
} catch(error) {
reject(error);
}
});
}
}
-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"
-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,5 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
namespace: lens-metrics
@@ -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
-50
View File
@@ -1,50 +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 {
name = 'user-mode';
latestVersion = "v2.0.0"
async install(cluster: Cluster): Promise<boolean> {
return super.install(cluster)
}
async upgrade(cluster: Cluster): Promise<boolean> {
return true
}
async featureStatus(kc: KubeConfig): Promise<FeatureStatus> {
return new Promise<FeatureStatus>( async (resolve, reject) => {
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
resolve(status)
} catch(error) {
resolve(status)
}
});
}
async uninstall(cluster: Cluster): Promise<boolean> {
return new Promise<boolean>(async (resolve, reject) => {
const rbacClient = cluster.contextHandler.kc.makeApiClient(RbacAuthorizationV1Api)
try {
await rbacClient.deleteClusterRole("lens-user");
await rbacClient.deleteClusterRoleBinding("lens-user");
resolve(true);
} catch(error) {
reject(error);
}
});
}
}
-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
-19
View File
@@ -1,19 +0,0 @@
import { autoUpdater } from "electron-updater"
import logger from "./logger"
export default class AppUpdater {
protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day
constructor() {
autoUpdater.logger = logger
}
public start() {
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify()
}, this.updateInterval)
return autoUpdater.checkForUpdatesAndNotify()
}
}
-289
View File
@@ -1,289 +0,0 @@
import { KubeConfig } from "@kubernetes/client-node"
import { PromiseIpc } from "electron-promise-ipc"
import * as http from "http"
import { Cluster, ClusterBaseInfo } from "./cluster"
import { clusterStore } from "../common/cluster-store"
import * as k8s from "./k8s"
import logger from "./logger"
import { LensProxy } from "./proxy"
import { app } from "electron"
import * as path from "path"
import { promises } from "fs"
import { ensureDir } from "fs-extra"
import * as filenamify from "filenamify"
import { v4 as uuid } from "uuid"
declare const __static: string;
export type FeatureInstallRequest = {
name: string;
clusterId: string;
config: any;
}
export type FeatureInstallResponse = {
success: boolean;
message: string;
}
export type ClusterIconUpload = {
path: string;
name: string;
clusterId: string;
}
export class ClusterManager {
public static readonly clusterIconDir = path.join(app.getPath("userData"), "icons")
protected promiseIpc: any
protected proxyServer: LensProxy
protected port: number
protected clusters: Map<string, Cluster>;
constructor(clusters: Cluster[], port: number) {
this.promiseIpc = new PromiseIpc({ timeout: 2000 })
this.port = port
this.clusters = new Map()
clusters.forEach((clusterInfo) => {
try {
const kc = this.loadKubeConfig(clusterInfo.kubeConfig)
logger.debug(`Starting to load target definitions for ${ kc.currentContext }`)
const cluster = new Cluster({
id: clusterInfo.id,
port: this.port,
kubeConfig: clusterInfo.kubeConfig,
preferences: clusterInfo.preferences,
workspace: clusterInfo.workspace
})
cluster.init(kc)
logger.debug(`Created cluster[id: ${ cluster.id }] for context ${ cluster.contextName }`)
this.clusters.set(cluster.id, cluster)
} catch(error) {
logger.error(`Error while initializing ${clusterInfo.contextName}`)
}
});
logger.debug("clusters after constructor:" + this.clusters.size)
this.listenEvents()
}
public getClusters() {
return this.clusters.values()
}
public getCluster(id: string) {
return this.clusters.get(id)
}
public stop() {
const clusters = Array.from(this.getClusters())
clusters.map(cluster => cluster.stopServer())
}
protected loadKubeConfig(config: string): KubeConfig {
const kc = new KubeConfig();
kc.loadFromString(config);
return kc;
}
protected async addNewCluster(clusterData: ClusterBaseInfo): Promise<Cluster> {
return new Promise(async (resolve, reject) => {
try {
const configs: KubeConfig[] = k8s.loadAndSplitConfig(clusterData.kubeConfig)
if(configs.length == 0) {
reject("No cluster contexts defined")
}
configs.forEach(c => {
k8s.valideConfig(c)
const cluster = new Cluster({
id: uuid(),
port: this.port,
kubeConfig: k8s.dumpConfigYaml(c),
preferences: clusterData.preferences,
workspace: clusterData.workspace
})
cluster.init(c)
cluster.save()
this.clusters.set(cluster.id, cluster)
resolve(cluster)
});
} catch(error) {
logger.error(error)
reject(error)
}
});
}
protected listenEvents() {
this.promiseIpc.on("addCluster", async (clusterData: ClusterBaseInfo) => {
logger.debug(`IPC: addCluster`)
const cluster = await this.addNewCluster(clusterData)
return {
addedCluster: this.clusterResponse(cluster),
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => this.clusterResponse(cluster))
}
});
this.promiseIpc.on("getClusters", async (workspaceId: string) => {
logger.debug(`IPC: getClusters, workspace ${workspaceId}`)
const workspaceClusters = Array.from(this.getClusters()).filter((cluster) => cluster.workspace === workspaceId)
return workspaceClusters.map((cluster: Cluster) => this.clusterResponse(cluster))
});
this.promiseIpc.on("getCluster", async (id: string) => {
logger.debug(`IPC: getCluster`)
const cluster = this.getCluster(id)
if (cluster) {
await cluster.refreshCluster()
return this.clusterResponse(cluster)
} else {
return null
}
});
this.promiseIpc.on("installFeature", async (installReq: FeatureInstallRequest) => {
logger.debug(`IPC: installFeature for ${installReq.name}`)
const cluster = this.clusters.get(installReq.clusterId)
try {
await cluster.installFeature(installReq.name, installReq.config)
return {success: true, message: ""}
} catch(error) {
return {success: false, message: error}
}
});
this.promiseIpc.on("upgradeFeature", async (installReq: FeatureInstallRequest) => {
logger.debug(`IPC: upgradeFeature for ${installReq.name}`)
const cluster = this.clusters.get(installReq.clusterId)
try {
await cluster.upgradeFeature(installReq.name, installReq.config)
return {success: true, message: ""}
} catch(error) {
return {success: false, message: error}
}
});
this.promiseIpc.on("uninstallFeature", async (installReq: FeatureInstallRequest) => {
logger.debug(`IPC: uninstallFeature for ${installReq.name}`)
const cluster = this.clusters.get(installReq.clusterId)
await cluster.uninstallFeature(installReq.name)
return {success: true, message: ""}
});
this.promiseIpc.on("saveClusterIcon", async (fileUpload: ClusterIconUpload) => {
logger.debug(`IPC: saveClusterIcon for ${fileUpload.clusterId}`)
const cluster = this.getCluster(fileUpload.clusterId)
if (!cluster) {
return {success: false, message: "Cluster not found"}
}
try {
const clusterIcon = await this.uploadClusterIcon(cluster, fileUpload.name, fileUpload.path)
clusterStore.reloadCluster(cluster);
if(!cluster.preferences) cluster.preferences = {};
cluster.preferences.icon = clusterIcon
clusterStore.storeCluster(cluster);
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
} catch(error) {
return {success: false, message: error}
}
});
this.promiseIpc.on("resetClusterIcon", async (id: string) => {
logger.debug(`IPC: resetClusterIcon`)
const cluster = this.getCluster(id)
if (cluster && cluster.preferences) {
cluster.preferences.icon = null;
clusterStore.storeCluster(cluster)
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
} else {
return {success: false, message: "Cluster not found"}
}
});
this.promiseIpc.on("refreshCluster", async (clusterId: string) => {
const cluster = this.clusters.get(clusterId)
await cluster.refreshCluster()
return this.clusterResponse(cluster)
});
this.promiseIpc.on("stopCluster", (clusterId: string) => {
logger.debug(`IPC: stopCluster: ${clusterId}`)
const cluster = this.clusters.get(clusterId)
if (cluster) {
cluster.stopServer()
return true
}
return false
});
this.promiseIpc.on("removeCluster", (ctx: string) => {
logger.debug(`IPC: removeCluster: ${ctx}`)
return this.removeCluster(ctx).map((cluster: Cluster) => this.clusterResponse(cluster))
});
this.promiseIpc.on("clusterStored", (clusterId: string) => {
logger.debug(`IPC: clusterStored: ${clusterId}`)
const cluster = this.clusters.get(clusterId)
if (cluster) {
clusterStore.reloadCluster(cluster);
cluster.stopServer()
}
});
this.promiseIpc.on("preferencesSaved", () => {
logger.debug(`IPC: preferencesSaved`)
this.clusters.forEach((cluster) => {
cluster.stopServer()
})
});
this.promiseIpc.on("getClusterEvents", async (clusterId: string) => {
const cluster = this.clusters.get(clusterId)
return cluster.getEventCount();
});
}
public removeCluster(id: string): Cluster[] {
const cluster = this.clusters.get(id)
if (cluster) {
cluster.stopServer()
clusterStore.removeCluster(cluster.id);
this.clusters.delete(cluster.id)
}
return Array.from(this.clusters.values())
}
public getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1]
if (clusterId) {
cluster = this.clusters.get(clusterId)
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, "/api-kube")
}
}
} else {
const id = req.headers.host.split(".")[0]
cluster = this.clusters.get(id)
}
return cluster;
}
// TODO: remove this
protected clusterResponse(cluster: Cluster) {
return cluster.toClusterInfo()
}
protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise<string> {
await ensureDir(ClusterManager.clusterIconDir)
fileName = filenamify(cluster.contextName + "-" + fileName)
const dest = path.join(ClusterManager.clusterIconDir, fileName)
await promises.copyFile(src, dest)
return "store:///icons/" + fileName
}
}
-323
View File
@@ -1,323 +0,0 @@
import { ContextHandler } from "./context-handler"
import { FeatureStatusMap } from "./feature"
import * as k8s from "./k8s"
import { clusterStore } from "../common/cluster-store"
import logger from "./logger"
import { KubeConfig, CoreV1Api, AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"
import * as fm from "./feature-manager";
import { Kubectl } from "./kubectl";
import { PromiseIpc } from "electron-promise-ipc"
import * as request from "request-promise-native"
import { KubeconfigManager } from "./kubeconfig-manager"
enum ClusterStatus {
AccessGranted = 2,
AccessDenied = 1,
Offline = 0
}
export interface ClusterBaseInfo {
id: string;
kubeConfig: string;
preferences?: ClusterPreferences;
port?: number;
workspace?: string;
}
export interface ClusterInfo extends ClusterBaseInfo {
url: string;
apiUrl: string;
online?: boolean;
accessible?: boolean;
failureReason?: string;
nodes?: number;
version?: string;
distribution?: string;
isAdmin?: boolean;
features?: FeatureStatusMap;
kubeCtl?: Kubectl;
contextName: string;
}
export type ClusterPreferences = {
terminalCWD?: string;
clusterName?: string;
prometheus?: {
namespace: string;
service: string;
port: number;
prefix: string;
};
prometheusProvider?: {
type: string;
};
icon?: string;
httpsProxy?: string;
}
export class Cluster implements ClusterInfo {
public id: string;
public workspace: string;
public contextHandler: ContextHandler;
public contextName: string;
public url: string;
public port: number;
public apiUrl: string;
public online: boolean;
public accessible: boolean;
public failureReason: string;
public nodes: number;
public version: string;
public distribution: string;
public isAdmin: boolean;
public features: FeatureStatusMap;
public kubeCtl: Kubectl
public kubeConfig: string;
public eventCount: number;
public preferences: ClusterPreferences;
protected eventPoller: NodeJS.Timeout;
protected promiseIpc = new PromiseIpc({ timeout: 2000 })
protected kubeconfigManager: KubeconfigManager;
constructor(clusterInfo: ClusterBaseInfo) {
if (clusterInfo) Object.assign(this, clusterInfo)
if (!this.preferences) this.preferences = {}
this.kubeconfigManager = new KubeconfigManager(this.kubeConfig)
}
public kubeconfigPath() {
return this.kubeconfigManager.getPath()
}
public async init(kc: KubeConfig) {
this.contextHandler = new ContextHandler(kc, this)
this.contextName = kc.currentContext
this.url = this.contextHandler.url
this.apiUrl = kc.getCurrentCluster().server
}
public stopServer() {
this.contextHandler.stopServer()
clearInterval(this.eventPoller);
}
public async installFeature(name: string, config: any) {
await fm.installFeature(name, this, config)
return this.refreshCluster()
}
public async upgradeFeature(name: string, config: any) {
await fm.upgradeFeature(name, this, config)
return this.refreshCluster()
}
public async uninstallFeature(name: string) {
await fm.uninstallFeature(name, this)
return this.refreshCluster()
}
public async refreshCluster() {
clusterStore.reloadCluster(this)
this.contextHandler.setClusterPreferences(this.preferences)
const connectionStatus = await this.getConnectionStatus()
if (connectionStatus == ClusterStatus.AccessGranted) {
this.accessible = true
} else {
this.accessible = false
}
if (connectionStatus > ClusterStatus.Offline) {
this.online = true
} else {
this.online = false
}
if (this.accessible) {
this.distribution = this.detectKubernetesDistribution(this.version)
this.features = await fm.getFeatures(this.contextHandler)
this.isAdmin = await this.isClusterAdmin()
this.nodes = await this.getNodeCount()
this.kubeCtl = new Kubectl(this.version)
this.kubeCtl.ensureKubectl()
}
this.eventCount = await this.getEventCount();
}
public updateKubeconfig(kubeconfig: string) {
const storedCluster = clusterStore.getCluster(this.id)
if (!storedCluster) { return }
this.kubeConfig = kubeconfig
this.save()
}
public getPrometheusApiPrefix() {
if (!this.preferences.prometheus?.prefix) {
return ""
}
return this.preferences.prometheus.prefix
}
public save() {
clusterStore.storeCluster(this)
}
public toClusterInfo(): ClusterInfo {
const clusterInfo: ClusterInfo = {
id: this.id,
workspace: this.workspace,
url: this.url,
contextName: this.contextHandler.kc.currentContext,
apiUrl: this.apiUrl,
online: this.online,
accessible: this.accessible,
failureReason: this.failureReason,
nodes: this.nodes,
version: this.version,
distribution: this.distribution,
isAdmin: this.isAdmin,
features: this.features,
kubeCtl: this.kubeCtl,
kubeConfig: this.kubeConfig,
preferences: this.preferences
}
return clusterInfo;
}
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
const options = Object.assign({
json: true, timeout: 10000
}, (opts || {}))
if (!options.headers) { options.headers = {} }
options.headers.host = `${this.id}.localhost:${this.port}`
return request(`http://127.0.0.1:${this.port}/api-kube${path}`, options)
}
protected async getConnectionStatus() {
try {
const response = await this.k8sRequest("/version")
this.version = response.gitVersion
this.failureReason = null
return ClusterStatus.AccessGranted;
} catch(error) {
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`)
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials";
return ClusterStatus.AccessDenied;
} else {
this.failureReason = error.error || error.message;
return ClusterStatus.Offline;
}
} else if (error.failed === true) {
if (error.timedOut === true) {
this.failureReason = "Connection timed out";
return ClusterStatus.Offline;
} else {
this.failureReason = "Failed to fetch credentials";
return ClusterStatus.AccessDenied;
}
}
this.failureReason = error.message;
return ClusterStatus.Offline;
}
}
public async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
const authApi = this.contextHandler.kc.makeApiClient(AuthorizationV1Api)
try {
const accessReview = await authApi.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes }
})
return accessReview.body.status.allowed === true
} catch(error) {
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
return false
}
}
protected async isClusterAdmin(): Promise<boolean> {
return this.canI({
namespace: "kube-system",
resource: "*",
verb: "create",
})
}
protected detectKubernetesDistribution(kubernetesVersion: string): string {
if (kubernetesVersion.includes("gke")) {
return "gke"
}
else if (kubernetesVersion.includes("eks")) {
return "eks"
}
else if (kubernetesVersion.includes("IKS")) {
return "iks"
}
else if(this.apiUrl.endsWith("azmk8s.io")) {
return "aks"
}
else if(this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
return "digitalocean"
}
else if (this.contextHandler.contextName.startsWith("minikube")) {
return "minikube"
}
else if (kubernetesVersion.includes("+")) {
return "custom"
}
return "vanilla"
}
protected async getNodeCount() {
try {
const response = await this.k8sRequest("/api/v1/nodes")
return response.items.length
} catch(error) {
logger.debug(`failed to request node list: ${error.message}`)
return null
}
}
public async getEventCount(): Promise<number> {
if (!this.isAdmin) {
return 0;
}
const client = this.contextHandler.kc.makeApiClient(CoreV1Api);
try {
const response = await client.listEventForAllNamespaces(false, null, null, null, 1000);
const uniqEventSources = new Set();
const warnings = response.body.items.filter(e => e.type !== 'Normal');
for (const w of warnings) {
if(w.involvedObject.kind === 'Pod') {
try {
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
if(k8s.podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid);
}
continue;
} catch (error) {
continue;
}
} else {
uniqEventSources.add(w.involvedObject.uid);
}
}
let nodeNotificationCount = 0;
const nodes = (await client.listNode()).body.items;
nodes.map(n => {
nodeNotificationCount = nodeNotificationCount + k8s.getNodeWarningConditions(n).length
});
return uniqEventSources.size + nodeNotificationCount;
} catch (error) {
logger.error("Failed to fetch event count: " + JSON.stringify(error))
return 0;
}
}
}
-210
View File
@@ -1,210 +0,0 @@
import { KubeConfig, CoreV1Api } from "@kubernetes/client-node"
import { readFileSync } from "fs"
import * as http from "http"
import { ServerOptions } from "http-proxy"
import * as url from "url"
import logger from "./logger"
import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy"
import { Cluster, ClusterPreferences } from "./cluster"
import { prometheusProviders } from "../common/prometheus-providers"
import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry"
import { PrometheusLens } from "./prometheus/lens"
export class ContextHandler {
public contextName: string
public id: string
public url: string
public kc: KubeConfig
public certData: string
public authCertData: string
public cluster: Cluster
protected apiTarget: ServerOptions
protected proxyTarget: ServerOptions
protected clusterUrl: url.UrlWithStringQuery
protected proxyServer: KubeAuthProxy
protected clientCert: string
protected clientKey: string
protected secureApiConnection = true
protected defaultNamespace: string
protected proxyPort: number
protected kubernetesApi: string
protected prometheusProvider: string
protected prometheusPath: string
protected clusterName: string
constructor(kc: KubeConfig, cluster: Cluster) {
this.id = cluster.id
this.kc = new KubeConfig()
this.kc.users = [
{
name: kc.getCurrentUser().name,
token: this.id
}
]
this.kc.contexts = [
{
name: kc.currentContext,
cluster: kc.getCurrentCluster().name,
user: kc.getCurrentUser().name,
namespace: kc.getContextObject(kc.currentContext).namespace
}
]
this.kc.setCurrentContext(kc.currentContext)
this.cluster = cluster
this.clusterUrl = url.parse(kc.getCurrentCluster().server)
this.contextName = kc.currentContext;
this.defaultNamespace = kc.getContextObject(kc.currentContext).namespace
this.url = `http://${this.id}.localhost:${cluster.port}/`
this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}`
this.kc.clusters = [
{
name: kc.getCurrentCluster().name,
server: this.kubernetesApi,
skipTLSVerify: true
}
]
this.setClusterPreferences(cluster.preferences)
}
public setClusterPreferences(clusterPreferences?: ClusterPreferences) {
this.prometheusProvider = clusterPreferences.prometheusProvider?.type
if (clusterPreferences && clusterPreferences.prometheus) {
const prom = clusterPreferences.prometheus
this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}`
} else {
this.prometheusPath = null
}
if(clusterPreferences && clusterPreferences.clusterName) {
this.clusterName = clusterPreferences.clusterName;
} else {
this.clusterName = this.contextName;
}
}
protected async resolvePrometheusPath(): Promise<string> {
const service = await this.getPrometheusService()
return `${service.namespace}/services/${service.service}:${service.port}`
}
public async getPrometheusProvider() {
if (!this.prometheusProvider) {
const service = await this.getPrometheusService()
logger.info(`using ${service.id} as prometheus provider`)
this.prometheusProvider = service.id
}
return prometheusProviders.find(p => p.id === this.prometheusProvider)
}
public async getPrometheusService(): Promise<PrometheusService> {
const providers = this.prometheusProvider ? prometheusProviders.filter((p, _) => p.id == this.prometheusProvider) : prometheusProviders
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
const apiClient = this.kc.makeApiClient(CoreV1Api)
return await provider.getPrometheusService(apiClient)
})
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
const service = resolvedPrometheusServices.filter(n => n)[0]
if (service) {
return service
} else {
return {
id: "lens",
namespace: "lens-metrics",
service: "prometheus",
port: 80
}
}
}
public async getPrometheusPath(): Promise<string> {
if (this.prometheusPath) return this.prometheusPath
this.prometheusPath = await this.resolvePrometheusPath()
return this.prometheusPath
}
public async getApiTarget(isWatchRequest = false) {
if (this.apiTarget && !isWatchRequest) {
return this.apiTarget
}
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000 // 4 hours for watch request, 30 seconds for the rest
const apiTarget = await this.newApiTarget(timeout)
if (!isWatchRequest) {
this.apiTarget = apiTarget
}
return apiTarget
}
protected async newApiTarget(timeout: number) {
return {
changeOrigin: true,
timeout: timeout,
headers: {
"Host": this.clusterUrl.hostname
},
target: {
port: await this.resolveProxyPort(),
protocol: "http://",
host: "localhost",
path: this.clusterUrl.path
},
}
}
protected async resolveProxyPort(): Promise<number> {
if (this.proxyPort) return this.proxyPort
let serverPort: number = null
try {
serverPort = await getFreePort()
} catch(error) {
logger.error(error)
throw(error)
}
this.proxyPort = serverPort
return serverPort
}
public applyHeaders(req: http.IncomingMessage) {
req.headers["authorization"] = `Bearer ${this.id}`
}
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
try {
await callback(this.cluster.kubeconfigPath())
} catch(error) {
throw(error)
}
}
public async ensureServer() {
if (!this.proxyServer) {
const proxyPort = await this.resolveProxyPort()
const proxyEnv = Object.assign({}, process.env)
if (this.cluster.preferences && this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
}
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv)
await this.proxyServer.run()
}
}
public stopServer() {
if (this.proxyServer) {
this.proxyServer.exit()
this.proxyServer = null
}
}
public proxyServerError() {
if (!this.proxyServer) { return null }
return this.proxyServer.lastError
}
}
-53
View File
@@ -1,53 +0,0 @@
import { ContextHandler } from "./context-handler"
import logger from "./logger";
import { Cluster } from "./cluster";
import { Feature, FeatureStatusMap } from "./feature"
import { MetricsFeature } from "../features/metrics"
import { UserModeFeature } from "../features/user-mode"
const ALL_FEATURES: any = {
'metrics': new MetricsFeature(null),
'user-mode': new UserModeFeature(null),
}
export async function getFeatures(clusterContext: ContextHandler): Promise<FeatureStatusMap> {
return new Promise<FeatureStatusMap>(async (resolve, reject) => {
const result: FeatureStatusMap = {};
logger.debug(`features for ${clusterContext.contextName}`);
for (const key in ALL_FEATURES) {
logger.debug(`feature ${key}`);
if (ALL_FEATURES.hasOwnProperty(key)) {
logger.debug("getting feature status...");
const feature = ALL_FEATURES[key] as Feature;
const status = await feature.featureStatus(clusterContext.kc);
result[feature.name] = status
} else {
logger.error("ALL_FEATURES.hasOwnProperty(key) returned FALSE ?!?!?!?!")
}
}
logger.debug(`getFeatures resolving with features: ${JSON.stringify(result)}`);
resolve(result);
});
}
export async function installFeature(name: string, cluster: Cluster, config: any) {
const feature = ALL_FEATURES[name] as Feature
// TODO Figure out how to handle config stuff
await feature.install(cluster)
}
export async function upgradeFeature(name: string, cluster: Cluster, config: any) {
const feature = ALL_FEATURES[name] as Feature
// TODO Figure out how to handle config stuff
await feature.upgrade(cluster)
}
export async function uninstallFeature(name: string, cluster: Cluster) {
const feature = ALL_FEATURES[name] as Feature
await feature.uninstall(cluster)
}
-104
View File
@@ -1,104 +0,0 @@
import * as fs from "fs";
import * as path from "path"
import * as hb from "handlebars"
import { ResourceApplier } from "./resource-applier"
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node"
import logger from "./logger";
import { Cluster } from "./cluster";
export type FeatureStatus = {
currentVersion: string;
installed: boolean;
latestVersion: string;
canUpgrade: boolean;
// TODO We need bunch of other stuff too: upgradeable, latestVersion, ...
};
export type FeatureStatusMap = {
[name: string]: FeatureStatus;
}
export abstract class Feature {
name: string;
config: any;
latestVersion: string;
constructor(config: any) {
if(config) {
this.config = config;
}
}
// TODO Return types for these?
async install(cluster: Cluster): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
// Read and process yamls through handlebar
const resources = this.renderTemplates();
// Apply processed manifests
cluster.contextHandler.withTemporaryKubeconfig(async (kubeconfigPath) => {
const resourceApplier = new ResourceApplier(cluster, kubeconfigPath)
try {
await resourceApplier.kubectlApplyAll(resources)
resolve(true)
} catch(error) {
reject(error)
}
});
});
}
abstract async upgrade(cluster: Cluster): Promise<boolean>;
abstract async uninstall(cluster: Cluster): Promise<boolean>;
abstract async featureStatus(kc: KubeConfig): Promise<FeatureStatus>;
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 req = await nsWatch.watch('/api/v1/namespaces', {resourceVersion: nsVersion, fieldSelector: "metadata.name=lens-metrics"},
(type, obj) => {
if(type === 'DELETED') {
logger.debug(`namespace ${name} finally gone`)
req.abort();
resolve()
}
},
(err) => {
if(err) {
reject(err)
}
});
});
}
protected renderTemplates(): string[] {
console.log("starting to render resources...");
const resources: string[] = [];
fs.readdirSync(this.manifestPath()).forEach((f) => {
const file = path.join(this.manifestPath(), f);
console.log("processing file:", file)
const raw = fs.readFileSync(file);
console.log("raw file loaded");
if(f.endsWith('.hb')) {
console.log("processing HB template");
const template = hb.compile(raw.toString());
resources.push(template(this.config));
console.log("HB template done");
} else {
console.log("using as raw, no HB detected");
resources.push(raw.toString());
}
});
return resources;
}
protected manifestPath() {
return path.join(__dirname, '..', 'features', this.name);
}
}
-11
View File
@@ -1,11 +0,0 @@
import * as fs from "fs"
export function ensureDir(dirname: string) {
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname)
}
}
export function randomFileName(name: string) {
return `${Math.random().toString(36).substring(2, 15)}-${Math.random().toString(36).substring(2, 15)}-${name}`
}
-114
View File
@@ -1,114 +0,0 @@
import { LensApiRequest } from "./router"
import { helmService } from "./helm-service"
import { LensApi } from "./lens-api"
import logger from "./logger"
class HelmApi extends LensApi {
public async listCharts(request: LensApiRequest) {
const { response } = request
const charts = await helmService.listCharts()
this.respondJson(response, charts)
}
public async getChart(request: LensApiRequest) {
const { params, query, response } = request
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"))
this.respondJson(response, chart)
}
public async getChartValues(request: LensApiRequest) {
const { params, query, response } = request
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"))
this.respondJson(response, values)
}
public async installChart(request: LensApiRequest) {
const { payload, cluster, response } = request
try {
const result = await helmService.installChart(cluster, payload)
this.respondJson(response, result, 201)
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
}
}
public async updateRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request
try {
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload )
this.respondJson(response, result)
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
}
}
public async rollbackRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request
try {
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision)
this.respondJson(response, result)
} catch (error) {
logger.debug(error)
this.respondText(response, error)
}
}
public async listReleases(request: LensApiRequest) {
const { cluster, params, response } = request
try {
const result = await helmService.listReleases(cluster, params.namespace)
this.respondJson(response, result)
} catch(error) {
logger.debug(error)
this.respondText(response, error)
}
}
public async getRelease(request: LensApiRequest) {
const { cluster, params, response } = request
try {
const result = await helmService.getRelease(cluster, params.release, params.namespace)
this.respondJson(response, result)
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
}
}
public async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response } = request
try {
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace)
this.respondText(response, result)
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
}
}
public async getReleaseHistory(request: LensApiRequest) {
const { cluster, params, response } = request
try {
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace)
this.respondJson(response, result)
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
}
}
public async deleteRelease(request: LensApiRequest) {
const { cluster, params, response } = request
try {
const result = await helmService.deleteRelease(cluster, params.release, params.namespace)
this.respondJson(response, result)
} catch (error) {
logger.debug(error)
this.respondText(response, error, 422)
}
}
}
export const helmApi = new HelmApi()
-73
View File
@@ -1,73 +0,0 @@
import * as fs from "fs";
import * as yaml from "js-yaml";
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
import logger from "./logger";
import { promiseExec } from "./promise-exec"
import { helmCli } from "./helm-cli"
type CachedYaml = {
entries: any;
}
export class HelmChartManager {
protected cache: any
protected repo: HelmRepo
constructor(repo: HelmRepo){
this.cache = HelmRepoManager.cache
this.repo = repo
}
public async chart(name: string) {
const charts = await this.charts()
return charts[name]
}
public async charts(): Promise<any> {
try {
const cachedYaml = await this.cachedYaml()
return cachedYaml["entries"]
} catch(error) {
logger.error(error)
return []
}
}
public async getReadme(name: string, version = "") {
const helm = await helmCli.binaryPath()
if(version && version != "") {
const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr)})
return stdout
} else {
const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr)})
return stdout
}
}
public async getValues(name: string, version = "") {
const helm = await helmCli.binaryPath()
if(version && version != "") {
const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr)})
return stdout
} else {
const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr)})
return stdout
}
}
protected async cachedYaml(): Promise<CachedYaml> {
if (!(this.repo.name in this.cache)) {
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, 'utf-8')
const data = yaml.safeLoad(cacheFile)
for(const key in data["entries"]) {
data["entries"][key].forEach((version: any) => {
version['repo'] = this.repo.name
version['created'] = Date.parse(version.created).toString()
})
}
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data))
}
return JSON.parse(this.cache[this.repo.name].toString())
}
}
-44
View File
@@ -1,44 +0,0 @@
import * as path from "path"
import { LensBinary, LensBinaryOpts } from "./lens-binary"
export class HelmCli extends LensBinary {
public constructor(baseDir: string, version: string) {
const opts: LensBinaryOpts = {
version,
baseDir: baseDir,
originalBinaryName: "helm",
newBinaryName: "helm3"
}
super(opts)
}
protected getTarName(): string|null {
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
}
protected getUrl() {
return `https://get.helm.sh/helm-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
}
protected getBinaryPath() {
return path.join(this.dirname, this.binaryName)
}
protected getOriginalBinaryPath() {
return path.join(this.dirname, this.platformName+"-"+this.arch, this.originalBinaryName)
}
}
const helmVersion = require("../../package.json").config.bundledHelmVersion
const isDevelopment = process.env.NODE_ENV !== "production"
let baseDir: string = null
if(isDevelopment) {
baseDir = path.join(process.cwd(), "binaries", "client")
} else {
baseDir = path.join(process.resourcesPath)
}
export const helmCli = new HelmCli(baseDir, helmVersion)
-112
View File
@@ -1,112 +0,0 @@
import * as tempy from "tempy";
import * as fs from "fs";
import * as yaml from "js-yaml";
import * as camelcaseKeys from "camelcase-keys";
import { promiseExec} from "./promise-exec"
import { helmCli } from "./helm-cli";
import { Cluster } from "./cluster";
export class HelmReleaseManager {
public async listReleases(pathToKubeconfig: string, namespace?: string) {
const helm = await helmCli.binaryPath()
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"
const { stdout, stderr } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
const output = JSON.parse(stdout)
if (output.length == 0) {
return output
}
const result: any = []
output.forEach((release: any, index: number) => {
output[index] = camelcaseKeys(release)
});
return output
}
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
const helm = await helmCli.binaryPath()
const fileName = tempy.file({name: "values.yaml"})
await fs.promises.writeFile(fileName, yaml.safeDump(values))
try {
let generateName = ""
if (!name) {
generateName = "--generate-name"
name = ""
}
const { stdout, stderr } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr)})
const releaseName = stdout.split("\n")[0].split(' ')[1].trim()
return {
log: stdout,
release: {
name: releaseName,
namespace: namespace
}
}
} finally {
await fs.promises.unlink(fileName)
}
}
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
const helm = await helmCli.binaryPath()
const fileName = tempy.file({name: "values.yaml"})
await fs.promises.writeFile(fileName, yaml.safeDump(values))
try {
const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.kubeconfigPath()}`).catch((error) => { throw(error.stderr)})
return {
log: stdout,
release: this.getRelease(name, namespace, cluster)
}
} finally {
await fs.promises.unlink(fileName)
}
}
public async getRelease(name: string, namespace: string, cluster: Cluster) {
const helm = await helmCli.binaryPath()
const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.kubeconfigPath()}`).catch((error) => { throw(error.stderr)})
const release = JSON.parse(stdout)
release.resources = await this.getResources(name, namespace, cluster)
return release
}
public async deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const { stdout, stderr } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
return stdout
}
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const { stdout, stderr } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
return stdout
}
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const {stdout, stderr} = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
return JSON.parse(stdout)
}
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath()
const {stdout, stderr} = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
return stdout
}
protected async getResources(name: string, namespace: string, cluster: Cluster) {
const helm = await helmCli.binaryPath()
const kubectl = await cluster.kubeCtl.kubectlPath()
const pathToKubeconfig = cluster.kubeconfigPath()
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => {
return { stdout: JSON.stringify({items: []})}
})
return stdout
}
}
export const releaseManager = new HelmReleaseManager()
-155
View File
@@ -1,155 +0,0 @@
import * as fs from "fs";
import logger from "./logger";
import * as yaml from "js-yaml";
import { promiseExec } from "./promise-exec";
import { helmCli } from "./helm-cli";
type HelmEnv = {
[key: string]: string | undefined;
}
export type HelmRepo = {
name: string;
url: string;
cacheFilePath?: string;
}
export class HelmRepoManager {
private static instance: HelmRepoManager;
public static cache = {}
protected helmEnv: HelmEnv
protected initialized: boolean
static getInstance(): HelmRepoManager {
if(!HelmRepoManager.instance) {
HelmRepoManager.instance = new HelmRepoManager()
}
return HelmRepoManager.instance;
}
private constructor() {
// use singleton getInstance()
}
public async init() {
const helm = await helmCli.binaryPath()
if (!this.initialized) {
this.helmEnv = await this.parseHelmEnv()
await this.update()
this.initialized = true
}
}
protected async parseHelmEnv() {
const helm = await helmCli.binaryPath()
const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => { throw(error.stderr)})
const lines = stdout.split(/\r?\n/) // split by new line feed
const env: HelmEnv = {}
lines.forEach((line: string) => {
const [key, value] = line.split("=")
if (key && value) {
env[key] = value.replace(/"/g, "") // strip quotas
}
})
return env
}
public async repositories(): Promise<Array<HelmRepo>> {
if(!this.initialized) {
await this.init()
}
const repositoryFilePath = this.helmEnv.HELM_REPOSITORY_CONFIG
const repoFile = await fs.promises.readFile(repositoryFilePath, 'utf8').catch(async (error) => {
return null
})
if(!repoFile) {
await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" })
return await this.repositories()
}
try {
const repositories = yaml.safeLoad(repoFile)
const result = repositories['repositories'].map((repository: HelmRepo) => {
return {
name: repository.name,
url: repository.url,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repository['name']}-index.yaml`
}
});
if (result.length == 0) {
await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" })
return await this.repositories()
}
return result
} catch (error) {
logger.debug(error)
return []
}
}
public async repository(name: string) {
const repositories = await this.repositories()
return repositories.find((repo: HelmRepo) => {
return repo.name == name
})
}
public async update() {
const helm = await helmCli.binaryPath()
logger.debug(`${helm} repo update`)
const {stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => { return { stdout: error.stdout } })
return stdout
}
protected async addRepositories(repositories: HelmRepo[]){
const currentRepositories = await this.repositories()
repositories.forEach(async (repo: HelmRepo) => {
try {
const repoExists = currentRepositories.find((currentRepo: HelmRepo) => {
return currentRepo.url == repo.url
})
if(!repoExists) {
await this.addRepo(repo)
}
}
catch(error) {
logger.error(JSON.stringify(error))
}
});
}
protected async pruneRepositories(repositoriesToKeep: HelmRepo[]) {
const repositories = await this.repositories()
repositories.filter((repo: HelmRepo) => {
return repositoriesToKeep.find((repoToKeep: HelmRepo) => {
return repo.name == repoToKeep.name
}) === undefined
}).forEach(async (repo: HelmRepo) => {
try {
const output = await this.removeRepo(repo)
logger.debug(output)
} catch(error) {
logger.error(error)
}
})
}
public async addRepo(repository: HelmRepo) {
const helm = await helmCli.binaryPath()
logger.debug(`${helm} repo add ${repository.name} ${repository.url}`)
const {stdout } = await promiseExec(`"${helm}" repo add ${repository.name} ${repository.url}`).catch((error) => { throw(error.stderr)})
return stdout
}
public async removeRepo(repository: HelmRepo): Promise<string> {
const helm = await helmCli.binaryPath()
logger.debug(`${helm} repo remove ${repository.name} ${repository.url}`)
const { stdout, stderr } = await promiseExec(`"${helm}" repo remove ${repository.name}`).catch((error) => { throw(error.stderr)})
return stdout
}
}
export const repoManager = HelmRepoManager.getInstance()
-105
View File
@@ -1,105 +0,0 @@
import { Cluster } from "./cluster";
import logger from "./logger";
import { repoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager";
import { releaseManager } from "./helm-release-manager";
class HelmService {
public async installChart(cluster: Cluster, data: {chart: string; values: {}; name: string; namespace: string; version: string}) {
const installResult = await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.kubeconfigPath())
return installResult
}
public async listCharts() {
const charts: any = {}
await repoManager.init()
const repositories = await repoManager.repositories()
for (const repo of repositories) {
charts[repo.name] = {}
const manager = new HelmChartManager(repo)
let entries = await manager.charts()
entries = this.excludeDeprecated(entries)
for(const key in entries) {
entries[key] = entries[key][0]
}
charts[repo.name] = entries
}
return charts
}
public async getChart(repoName: string, chartName: string, version = "") {
const result = {
readme: "",
versions: {}
}
const repo = await repoManager.repository(repoName)
const chartManager = new HelmChartManager(repo)
const chart = await chartManager.chart(chartName)
result.readme = await chartManager.getReadme(chartName, version)
result.versions = chart
return result
}
public async getChartValues(repoName: string, chartName: string, version = "") {
const repo = await repoManager.repository(repoName)
const chartManager = new HelmChartManager(repo)
return chartManager.getValues(chartName, version)
}
public async listReleases(cluster: Cluster, namespace: string = null) {
await repoManager.init()
const releases = await releaseManager.listReleases(cluster.kubeconfigPath(), namespace)
return releases
}
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release")
const release = await releaseManager.getRelease(releaseName, namespace, cluster)
return release
}
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release values")
const values = await releaseManager.getValues(releaseName, namespace, cluster.kubeconfigPath())
return values
}
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release history")
const history = await releaseManager.getHistory(releaseName, namespace, cluster.kubeconfigPath())
return(history)
}
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Delete release")
const release = await releaseManager.deleteRelease(releaseName, namespace, cluster.kubeconfigPath())
return release
}
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: {chart: string; values: {}; version: string}) {
logger.debug("Upgrade release")
const release = await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster)
return release
}
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
logger.debug("Rollback release")
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.kubeconfigPath())
return({ message: output })
}
protected excludeDeprecated(entries: any) {
for(const key in entries) {
entries[key] = entries[key].filter((entry: any) => {
if(Array.isArray(entry)) {
return entry[0]['deprecated'] != true
}
return entry["deprecated"] != true
})
}
return entries
}
}
export const helmService = new HelmService()
-127
View File
@@ -1,127 +0,0 @@
import "../common/system-ca"
import { app, dialog, protocol } from "electron"
import { PromiseIpc } from "electron-promise-ipc"
import * as path from "path"
import { format as formatUrl } from "url"
import logger from "./logger"
import initMenu from "./menu"
import * as proxy from "./proxy"
import { WindowManager } from "./window-manager";
import { clusterStore } from "../common/cluster-store"
import { tracker } from "./tracker"
import { ClusterManager } from "./cluster-manager";
import AppUpdater from "./app-updater"
import { shellSync } from "./shell-sync"
import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env"
import { findMainWebContents } from "./webcontents"
import "../common/prometheus-providers"
mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
}
const isDevelopment = process.env.NODE_ENV !== "production"
const promiseIpc = new PromiseIpc({ timeout: 2000 })
let windowManager: WindowManager = null;
let clusterManager: ClusterManager = null;
const vmURL = (isDevelopment) ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}` : formatUrl({
pathname: path.join(__dirname, "index.html"),
protocol: "file",
slashes: true,
})
async function main() {
await shellSync()
const updater = new AppUpdater()
updater.start();
tracker.event("app", "start");
protocol.registerFileProtocol('store', (request, callback) => {
const url = request.url.substr(8)
callback( path.normalize(`${app.getPath("userData")}/${url}`) )
}, (error) => {
if (error) console.error('Failed to register protocol')
})
let port: number = null
// find free port
try {
port = await getFreePort()
} catch (error) {
logger.error(error)
await dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
app.quit();
}
// create cluster manager
clusterManager = new ClusterManager(clusterStore.getAllClusterObjects(), port)
// run proxy
try {
proxy.listen(port, clusterManager)
} catch (error) {
logger.error(`Could not start proxy (127.0.0:${port}): ${error.message}`)
await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${port}): ${error.message || "unknown error"}`)
app.quit();
}
// boot windowmanager
windowManager = new WindowManager();
windowManager.showMain(vmURL)
initMenu({
logoutHook: async () => {
// IPC send needs webContents as we're sending it to renderer
promiseIpc.send('logout', findMainWebContents(), {}).then((data: any) => {
logger.debug("logout IPC sent");
})
},
showPreferencesHook: async () => {
// IPC send needs webContents as we're sending it to renderer
promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => {
logger.debug("navigate: preferences IPC sent");
})
},
addClusterHook: async () => {
promiseIpc.send('navigate', findMainWebContents(), { name: "add-cluster-page" }).then((data: any) => {
logger.debug("navigate: add-cluster-page IPC sent");
})
},
showWhatsNewHook: async () => {
promiseIpc.send('navigate', findMainWebContents(), { name: "whats-new-page" }).then((data: any) => {
logger.debug("navigate: whats-new-page IPC sent");
})
},
clusterSettingsHook: async () => {
promiseIpc.send('navigate', findMainWebContents(), { name: "cluster-settings-page" }).then((data: any) => {
logger.debug("navigate: cluster-settings-page IPC sent");
})
},
}, promiseIpc)
}
app.on("ready", main)
app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform != 'darwin') {
app.quit();
} else {
windowManager = null
if (clusterManager) clusterManager.stop()
}
})
app.on("activate", () => {
if (!windowManager) {
logger.debug("activate main window")
windowManager = new WindowManager(false)
windowManager.showMain(vmURL)
}
})
app.on("will-quit", async (event) => {
event.preventDefault(); // To allow mixpanel sending to be executed
if (clusterManager) clusterManager.stop()
app.exit(0);
})
-155
View File
@@ -1,155 +0,0 @@
import * as k8s from "@kubernetes/client-node"
import * as os from "os"
import { all } from "q";
import * as yaml from "js-yaml"
import { V1beta1ValidatingWebhookConfiguration } from "@kubernetes/client-node";
import logger from "./logger";
const kc = new k8s.KubeConfig()
function resolveTilde(filePath: string) {
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
return filePath.replace("~", os.homedir());
}
return filePath;
}
export function loadConfig(kubeconfig: string): k8s.KubeConfig {
if (kubeconfig) {
kc.loadFromFile(resolveTilde(kubeconfig))
} else {
kc.loadFromDefault();
}
return kc
}
/**
* KubeConfig is valid when there's atleast one of each defined:
* - User
* - Cluster
* - Context
*
* @param config KubeConfig to check
*/
export function valideConfig(config: k8s.KubeConfig): boolean {
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
if(!config.users || config.users.length == 0) {
throw new Error("No users provided in config")
}
if(!config.clusters || config.clusters.length == 0) {
throw new Error("No clusters provided in config")
}
if(!config.contexts || config.contexts.length == 0) {
throw new Error("No contexts provided in config")
}
return true
}
/**
* Breaks kube config into several configs. Each context as it own KubeConfig object
*
* @param configString yaml string of kube config
*/
export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] {
const configs: k8s.KubeConfig[] = []
if(!kubeConfig.contexts) {
return configs;
}
kubeConfig.contexts.forEach(ctx => {
const kc = new k8s.KubeConfig();
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n)
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n)
kc.setCurrentContext(ctx.name);
configs.push(kc);
});
return configs;
}
/**
* Loads KubeConfig from a yaml string and breaks it into several configs. Each context
*
* @param configString yaml string of kube config
*/
export function loadAndSplitConfig(configString: string): k8s.KubeConfig[] {
const allConfigs = new k8s.KubeConfig();
allConfigs.loadFromString(configString);
return splitConfig(allConfigs);
}
export function dumpConfigYaml(kc: k8s.KubeConfig): string {
const config = {
apiVersion: "v1",
kind: "Config",
preferences: {},
'current-context': kc.currentContext,
clusters: kc.clusters.map(c => {
return {
name: c.name,
cluster: {
'certificate-authority-data': c.caData,
'certificate-authority': c.caFile,
server: c.server,
'insecure-skip-tls-verify': c.skipTLSVerify
}
}
}),
contexts: kc.contexts.map(c => {
return {
name: c.name,
context: {
cluster: c.cluster,
user: c.user,
namespace: c.namespace
}
}
}),
users: kc.users.map(u => {
return {
name: u.name,
user: {
'client-certificate-data': u.certData,
'client-certificate': u.certFile,
'client-key-data': u.keyData,
'client-key': u.keyFile,
'auth-provider': u.authProvider,
exec: u.exec,
token: u.token,
username: u.username,
password: u.password
}
}
})
}
console.log("dumping kc:", config);
// skipInvalid: true makes dump ignore undefined values
return yaml.safeDump(config, {skipInvalid: true});
}
export function podHasIssues(pod: k8s.V1Pod) {
// Logic adapted from dashboard
const notReady = !!pod.status.conditions.find(condition => {
return condition.type == "Ready" && condition.status !== "True"
});
return (
notReady ||
pod.status.phase !== "Running" ||
pod.spec.priority > 500000 // We're interested in high prio pods events regardless of their running status
)
}
// Logic adapted from dashboard
// see: https://github.com/kontena/kontena-k8s-dashboard/blob/7d8f9cb678cc817a22dd1886c5e79415b212b9bf/client/api/endpoints/nodes.api.ts#L147
export function getNodeWarningConditions(node: k8s.V1Node) {
return node.status.conditions.filter(c =>
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
)
}
-108
View File
@@ -1,108 +0,0 @@
import { spawn, ChildProcess } from "child_process"
import logger from "./logger"
import * as tcpPortUsed from "tcp-port-used"
import { Kubectl, bundledKubectl } from "./kubectl"
import { Cluster } from "./cluster"
import { readFileSync, watch } from "fs"
import { PromiseIpc } from "electron-promise-ipc"
import { findMainWebContents } from "./webcontents"
import * as url from "url"
export class KubeAuthProxy {
public lastError: string
protected cluster: Cluster
protected env: NodeJS.ProcessEnv = null
protected proxyProcess: ChildProcess
protected port: number
protected kubectl: Kubectl
protected promiseIpc: any
constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) {
this.env = env
this.port = port
this.cluster = cluster
this.kubectl = bundledKubectl
this.promiseIpc = new PromiseIpc({ timeout: 2000 })
}
public async run(): Promise<void> {
if (this.proxyProcess) {
return new Promise((resolve, reject) => {
resolve()
})
}
const proxyBin = await this.kubectl.kubectlPath()
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => {
if (eventType === "change") {
const kc = readFileSync(this.cluster.kubeconfigPath()).toString()
if (kc.trim().length > 0) { // Prevent updating empty configs back to store
this.cluster.updateKubeconfig(kc)
} else {
logger.warn(`kubeconfig watch on ${this.cluster.kubeconfigPath()} resulted into empty config, ignoring...`)
}
}
})
configWatcher.on("error", () => {})
const clusterUrl = url.parse(this.cluster.apiUrl)
let args = [
"proxy",
"-p", this.port.toString(),
"--kubeconfig", this.cluster.kubeconfigPath(),
"--accept-hosts", clusterUrl.hostname,
]
if (process.env.DEBUG_PROXY === "true") {
args = args.concat(["-v", "9"])
}
this.proxyProcess = spawn(proxyBin, args, {
env: this.env
})
this.proxyProcess.on("exit", (code) => {
logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`)
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((_) => {})
this.proxyProcess = null
configWatcher.close()
})
this.proxyProcess.stdout.on('data', (data) => {
let logItem = data.toString()
if (logItem.startsWith("Starting to serve on")) {
logItem = "Authentication proxy started\n"
}
logger.debug(`proxy ${this.cluster.contextName} stdout: ${logItem}`)
this.sendIpcLogMessage(logItem, "stdout")
})
this.proxyProcess.stderr.on('data', (data) => {
this.lastError = this.parseError(data.toString())
logger.debug(`proxy ${this.cluster.contextName} stderr: ${data}`)
this.sendIpcLogMessage(data.toString(), "stderr")
})
return tcpPortUsed.waitUntilUsed(this.port, 500, 10000)
}
protected parseError(data: string) {
const error = data.split("http: proxy error:").slice(1).join("").trim()
let errorMsg = error
const jsonError = error.split("Response: ")[1]
if (jsonError) {
try {
const parsedError = JSON.parse(jsonError)
errorMsg = parsedError.error_description || parsedError.error || jsonError
} catch(_) {
errorMsg = jsonError.trim()
}
}
return errorMsg
}
protected async sendIpcLogMessage(data: string, stream: string) {
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data: data, stream: stream })
}
public exit() {
if (this.proxyProcess) {
logger.debug(`Stopping local proxy: ${this.cluster.contextName}`)
this.proxyProcess.kill()
}
}
}
-32
View File
@@ -1,32 +0,0 @@
import { app } from "electron"
import * as fs from "fs"
import { ensureDir, randomFileName} from "./file-helpers"
import logger from "./logger"
export class KubeconfigManager {
protected configDir = app.getPath("temp")
protected kubeconfig: string
protected tempFile: string
constructor(kubeconfig: string) {
this.kubeconfig = kubeconfig
this.tempFile = this.createTemporaryKubeconfig()
}
public getPath() {
return this.tempFile
}
protected createTemporaryKubeconfig(): string {
ensureDir(this.configDir)
const path = `${this.configDir}/${randomFileName("kubeconfig")}`
logger.debug('Creating temporary kubeconfig: ' + path)
fs.writeFileSync(path, this.kubeconfig)
return path
}
public unlink() {
logger.debug('Deleting temporary kubeconfig: ' + this.tempFile)
fs.unlinkSync(this.tempFile)
}
}
-310
View File
@@ -1,310 +0,0 @@
import { app, remote } from "electron"
import * as path from "path"
import * as fs from "fs"
import * as request from "request"
import * as requestPromise from "request-promise-native"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import * as md5File from "md5-file"
import { globalRequestOpts } from "../common/request"
import * as lockFile from "proper-lockfile"
import { helmCli } from "./helm-cli"
import { userStore } from "../common/user-store"
const bundledVersion = require("../../package.json").config.bundledKubectlVersion
const kubectlMap: Map<string, string> = new Map([
["1.7", "1.8.15"],
["1.8", "1.9.10"],
["1.9", "1.10.13"],
["1.10", "1.11.10"],
["1.11", "1.12.10"],
["1.12", "1.13.12"],
["1.13", "1.13.12"],
["1.14", "1.14.10"],
["1.15", "1.15.11"],
["1.16", "1.16.8"],
["1.17", bundledVersion],
["1.18", "1.18.0"]
])
const packageMirrors: Map<string, string> = new Map([
["default", "https://storage.googleapis.com/kubernetes-release/release"],
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
])
const initScriptVersionString = "# lens-initscript v3\n"
const isDevelopment = process.env.NODE_ENV !== "production"
let bundledPath: string = null
if(isDevelopment) {
bundledPath = path.join(process.cwd(), "binaries", "client", process.platform, process.arch, "kubectl")
} else {
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl")
}
if(process.platform === "win32") bundledPath = `${bundledPath}.exe`
export class Kubectl {
public kubectlVersion: string
protected directory: string
protected url: string
protected path: string
protected dirname: string
public static readonly kubectlDir = path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
public static readonly bundledKubectlPath = bundledPath
public static readonly bundledKubectlVersion: string = bundledVersion
private static bundledInstance: Kubectl;
// Returns the single bundled Kubectl instance
public static bundled() {
if(!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion)
return Kubectl.bundledInstance
}
constructor(clusterVersion: string) {
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion)
const minorVersion = versionParts[1]
/* minorVersion is the first two digits of kube server version
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
if(kubectlMap.has(minorVersion)) {
this.kubectlVersion = kubectlMap.get(minorVersion)
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using version map")
} else {
this.kubectlVersion = versionParts[1] + versionParts[2]
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using fallback")
}
let arch = null
if(process.arch == "x64") {
arch = "amd64"
} else if(process.arch == "x86" || process.arch == "ia32") {
arch = "386"
} else {
arch = process.arch
}
const platformName = process.platform === "win32" ? "windows" : process.platform
const binaryName = process.platform === "win32" ? "kubectl.exe" : "kubectl"
this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`
this.dirname = path.normalize(path.join(Kubectl.kubectlDir, this.kubectlVersion))
this.path = path.join(this.dirname, binaryName)
}
public async kubectlPath(): Promise<string> {
try {
await this.ensureKubectl()
return this.path
} catch(err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version")
logger.error(err)
return Kubectl.bundledKubectlPath
}
}
public async binDir() {
try {
await this.ensureKubectl()
return this.dirname
} catch(err) {
logger.error(err)
return ""
}
}
protected async urlEtag() {
const response = await requestPromise({
method: "HEAD",
uri: this.url,
resolveWithFullResponse: true,
timeout: 4000,
...this.getRequestOpts()
}).catch((error) => { logger.error(error) })
if (response && response.headers["etag"]) {
return response.headers["etag"].replace(/"/g, "")
}
return ""
}
public async checkBinary(checkTag = true) {
const exists = await pathExists(this.path)
if (exists) {
if (!checkTag) {
return true
}
const hash = md5File.sync(this.path)
const etag = await this.urlEtag()
if (etag === "") {
logger.debug("Cannot resolve kubectl remote etag")
return true
}
if (hash == etag) {
logger.debug("Kubectl md5sum matches the remote etag")
return true
}
logger.error("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again")
await fs.promises.unlink(this.path)
}
return false
}
protected async checkBundled(): Promise<boolean> {
if(this.kubectlVersion === Kubectl.bundledKubectlVersion) {
try {
const exist = await pathExists(this.path)
if (!exist) {
await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path)
await fs.promises.chmod(this.path, 0o755)
}
return true
} catch(err) {
logger.error("Could not copy the bundled kubectl to app-data: " + err)
return false
}
} else {
return false
}
}
public async ensureKubectl(): Promise<boolean> {
await ensureDir(this.dirname, 0o755)
return lockFile.lock(this.dirname).then(async (release) => {
logger.debug(`Acquired a lock for ${this.kubectlVersion}`)
const bundled = await this.checkBundled()
const isValid = await this.checkBinary(!bundled)
if(!isValid) {
await this.downloadKubectl().catch((error) => { logger.error(error) });
}
await this.writeInitScripts().catch((error) => { logger.error("Failed to write init scripts"); logger.error(error) })
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release()
return true
}).catch((e) => {
logger.error(`Failed to get a lock for ${this.kubectlVersion}`)
logger.error(e)
return false
})
}
public async downloadKubectl() {
await ensureDir(path.dirname(this.path), 0o755)
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`)
return new Promise((resolve, reject) => {
const stream = request({
gzip: true,
...this.getRequestOpts()
})
const file = fs.createWriteStream(this.path)
stream.on("complete", () => {
logger.debug("kubectl binary download finished")
file.end(() => {})
})
stream.on("error", (error) => {
logger.error(error)
fs.unlink(this.path, () => {})
reject(error)
})
file.on("close", () => {
logger.debug("kubectl binary download closed")
fs.chmod(this.path, 0o755, () => {})
resolve()
})
stream.pipe(file)
})
}
protected async scriptIsLatest(scriptPath: string) {
const scriptExists = await pathExists(scriptPath)
if(!scriptExists) return false
try {
const filehandle = await fs.promises.open(scriptPath, 'r')
const buffer = Buffer.alloc(40)
await filehandle.read(buffer, 0, 40, 0)
await filehandle.close()
return buffer.toString().startsWith(initScriptVersionString)
} catch (err) {
logger.error(err)
return false
}
}
protected async writeInitScripts() {
const helmPath = helmCli.getBinaryDir()
const fsPromises = fs.promises;
const bashScriptPath = path.join(this.dirname, '.bash_set_path')
const bashScriptIsLatest = await this.scriptIsLatest(bashScriptPath)
if(!bashScriptIsLatest) {
let bashScript = "" + initScriptVersionString
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n"
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n"
bashScript += " . \"$HOME/.bash_profile\"\n"
bashScript += "elif test -f \"$HOME/.bash_login\"; then\n"
bashScript += " . \"$HOME/.bash_login\"\n"
bashScript += "elif test -f \"$HOME/.profile\"; then\n"
bashScript += " . \"$HOME/.profile\"\n"
bashScript += "fi\n"
bashScript += `export PATH="${this.dirname}:${helmPath}:$PATH"\n`
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
bashScript += "unset tempkubeconfig\n"
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 })
}
const zshScriptPath = path.join(this.dirname, '.zlogin')
const zshScriptIsLatest = await this.scriptIsLatest(zshScriptPath)
if(!zshScriptIsLatest) {
let zshScript = "" + initScriptVersionString
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
// restore previous ZDOTDIR
zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n"
// source all the files
zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n"
zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n"
zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n"
zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n"
// voodoo to replace any previous occurences of kubectl path in the PATH
zshScript += `kubectlpath=\"${this.dirname}"\n`
zshScript += `helmpath=\"${helmPath}"\n`
zshScript += "p=\":$kubectlpath:\"\n"
zshScript += "d=\":$PATH:\"\n"
zshScript += "d=${d//$p/:}\n"
zshScript += "d=${d/#:/}\n"
zshScript += "export PATH=\"$kubectlpath:$helmpath:${d/%:/}\"\n"
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
zshScript += "unset tempkubeconfig\n"
zshScript += "unset OLD_ZDOTDIR\n"
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 })
}
}
protected getRequestOpts() {
return globalRequestOpts({
url: this.url
})
}
protected getDownloadMirror() {
if (process.platform == "darwin") {
return packageMirrors.get("default") // MacOS packages are only available from default
}
const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror)
if (mirror) { return mirror }
return packageMirrors.get("default")
}
}
const bundledKubectl = Kubectl.bundled()
export { bundledKubectl }
-17
View File
@@ -1,17 +0,0 @@
import * as http from "http";
export abstract class LensApi {
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
this.respond(res, JSON.stringify(content), "application/json", status)
}
protected respondText(res: http.ServerResponse, content: string, status = 200) {
this.respond(res, content, "text/plain", status)
}
protected respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
res.setHeader("Content-Type", contentType)
res.statusCode = status
res.end(content)
}
}
-172
View File
@@ -1,172 +0,0 @@
import * as path from "path"
import * as fs from "fs"
import * as request from "request"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import * as tar from "tar"
export type LensBinaryOpts = {
version: string;
baseDir: string;
originalBinaryName: string;
newBinaryName?: string;
requestOpts?: request.Options;
}
export class LensBinary {
public binaryVersion: string
protected directory: string
protected url: string
protected path: string;
protected tarPath: string;
protected dirname: string
protected binaryName: string
protected platformName: string
protected arch: string
protected originalBinaryName: string
protected requestOpts: request.Options
constructor(opts: LensBinaryOpts) {
const baseDir = opts.baseDir
this.originalBinaryName = opts.originalBinaryName
this.binaryName = opts.newBinaryName || opts.originalBinaryName
this.binaryVersion = opts.version
this.requestOpts = opts.requestOpts
let arch = null
if(process.arch == "x64") {
arch = "amd64"
} else if(process.arch == "x86" || process.arch == "ia32") {
arch = "386"
} else {
arch = process.arch
}
this.arch = arch
this.platformName = process.platform === "win32" ? "windows" : process.platform
this.dirname = path.normalize(path.join(baseDir, this.binaryName))
if (process.platform === "win32") {
this.binaryName = this.binaryName+".exe"
this.originalBinaryName = this.originalBinaryName+".exe"
}
const tarName = this.getTarName()
if (tarName) {
this.tarPath = path.join(this.dirname, tarName)
}
}
protected binaryDir() {
throw new Error("binaryDir not implemented")
}
public async binaryPath() {
await this.ensureBinary()
return this.getBinaryPath()
}
protected getTarName(): string|null {
return null
}
protected getUrl() {
return ""
}
protected getBinaryPath() {
return ""
}
protected getOriginalBinaryPath() {
return ""
}
public getBinaryDir() {
return path.dirname(this.getBinaryPath())
}
public async binDir() {
try {
await this.ensureBinary()
return this.dirname
} catch(err) {
logger.error(err)
return ""
}
}
protected async checkBinary() {
const exists = await pathExists(this.getBinaryPath())
return exists
}
public async ensureBinary() {
const isValid = await this.checkBinary()
if(!isValid) {
await this.downloadBinary().catch((error) => { logger.error(error) });
if (this.tarPath) await this.untarBinary()
if(this.originalBinaryName != this.binaryName ) await this.renameBinary()
logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
}
}
protected async untarBinary() {
return new Promise<void>((resolve, reject) => {
logger.debug(`Extracting ${this.originalBinaryName} binary`)
tar.x({
file: this.tarPath,
cwd: this.dirname
}).then((_ => {
resolve()
}))
})
}
protected async renameBinary() {
return new Promise<void>((resolve, reject) => {
logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`)
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath()
await ensureDir(this.getBinaryDir(), 0o755)
const file = fs.createWriteStream(binaryPath)
const url = this.getUrl()
logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`)
const requestOpts: request.UriOptions & request.CoreOptions = {
uri: url,
gzip: true,
...this.requestOpts
}
const stream = request(requestOpts)
stream.on("complete", () => {
logger.info(`Download of ${this.originalBinaryName} finished`)
file.end(() => {})
})
stream.on("error", (error) => {
logger.error(error)
fs.unlink(binaryPath, () => {})
throw(error)
})
return new Promise((resolve, reject) => {
file.on("close", () => {
logger.debug(`${this.originalBinaryName} binary download closed`)
if(!this.tarPath) fs.chmod(binaryPath, 0o755, () => {})
resolve()
})
stream.pipe(file)
})
}
}
-20
View File
@@ -1,20 +0,0 @@
import * as winston from "winston"
const options = {
colorize: true,
handleExceptions: false,
json: false,
level: process.env.DEBUG === "true" ? "debug" : "info",
}
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
transports: [
new winston.transports.Console(options),
],
});
export default logger
-217
View File
@@ -1,217 +0,0 @@
import {app, dialog, Menu, MenuItemConstructorOptions, shell, webContents, BrowserWindow, MenuItem} from "electron"
export interface MenuOptions {
logoutHook: any;
addClusterHook: any;
clusterSettingsHook: any;
showWhatsNewHook: any;
showPreferencesHook: any;
// all the above are really () => void type functions
}
function setClusterSettingsEnabled(enabled: boolean) {
const isMac = process.platform === 'darwin';
const menuIndex = isMac ? 1 : 0
Menu.getApplicationMenu().items[menuIndex].submenu.items[1].enabled = enabled
}
function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
const appDetails = [
`Version: ${app.getVersion()}`,
]
appDetails.push(`Copyright 2020 Lakend Labs, Inc.`)
let title = "Lens"
if (process.platform === "win32") {
title = ` ${title}`
}
dialog.showMessageBoxSync(browserWindow, {
title,
type: "info",
buttons: ["Close"],
message: `Lens`,
detail: appDetails.join("\r\n")
})
}
/**
* Constructs the menu based on the example at: https://electronjs.org/docs/api/menu#main-process
* Menu items are constructed piece-by-piece to have slightly better control on individual sub-menus
*
* @param ipc the main promiceIpc handle. Needed to be able to hook IPC sending into logout click handler.
*/
export default function initMenu(opts: MenuOptions, promiseIpc: any) {
const isMac = process.platform === 'darwin';
const isDevelopment = process.env.NODE_ENV === 'development';
const mt: MenuItemConstructorOptions[] = [];
const macAppMenu: MenuItemConstructorOptions = {
label: app.getName(),
submenu: [
{
label: "About Lens",
click: showAbout
},
{ type: 'separator' },
{
label: 'Preferences',
click: opts.showPreferencesHook,
enabled: true
},
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
};
if(isMac) {
mt.push(macAppMenu);
}
let fileMenu: MenuItemConstructorOptions;
if(isMac) {
fileMenu = {
label: 'File',
submenu: [{
label: 'Add Cluster...',
click: opts.addClusterHook,
},
{
label: 'Cluster Settings',
click: opts.clusterSettingsHook,
enabled: false
}
]
}
} else {
fileMenu = {
label: 'File',
submenu: [
{
label: 'Add Cluster...',
click: opts.addClusterHook,
},
{
label: 'Cluster Settings',
click: opts.clusterSettingsHook,
enabled: false
},
{ type: 'separator' },
{
label: 'Preferences',
click: opts.showPreferencesHook,
enabled: true
},
{ type: 'separator' },
{ role: 'quit' }
]
}
}
mt.push(fileMenu);
const editMenu: MenuItemConstructorOptions = {
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' },
]
};
mt.push(editMenu);
const viewMenu: MenuItemConstructorOptions = {
label: 'View',
submenu: [
{
label: 'Back',
accelerator: 'CmdOrCtrl+[',
click () {
webContents.getFocusedWebContents().executeJavaScript('window.history.back()')
}
},
{
label: 'Forward',
accelerator: 'CmdOrCtrl+]',
click () {
webContents.getFocusedWebContents().executeJavaScript('window.history.forward()')
}
},
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click () {
webContents.getFocusedWebContents().reload()
}
},
...(isDevelopment ? [
{ role: 'toggleDevTools'} as MenuItemConstructorOptions,
{
label: 'Open Dashboard Devtools',
click() {
webContents.getFocusedWebContents().openDevTools()
}
}
] : []),
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
};
mt.push(viewMenu);
const helpMenu: MenuItemConstructorOptions = {
role: 'help',
submenu: [
{
label: 'License',
click: async () => {
shell.openExternal('https://lakendlabs.com/licenses/lens-eula.md');
},
},
{
label: 'Community Slack',
click: async () => {
shell.openExternal('https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI');
},
},
{
label: 'Report an Issue',
click: async () => {
shell.openExternal('https://github.com/lensapp/lens/issues');
},
},
{
label: "What's new?",
click: opts.showWhatsNewHook,
},
...(process.platform !== "darwin" ? [{
label: "About Lens",
click: showAbout
} as MenuItemConstructorOptions] : [])
]
};
mt.push(helpMenu);
const menu = Menu.buildFromTemplate(mt);
Menu.setApplicationMenu(menu);
promiseIpc.on("enableClusterSettingsMenuItem", (clusterId: string) => {
setClusterSettingsEnabled(true)
});
promiseIpc.on("disableClusterSettingsMenuItem", () => {
setClusterSettingsEnabled(false)
});
};
-144
View File
@@ -1,144 +0,0 @@
import * as WebSocket from "ws"
import * as pty from "node-pty"
import { ShellSession } from "./shell-session";
import { v4 as uuid } from "uuid"
import * as k8s from "@kubernetes/client-node"
import logger from "./logger";
import { KubeConfig, V1Pod } from "@kubernetes/client-node";
import { tracker } from "./tracker"
import { Cluster, ClusterPreferences } from "./cluster"
export class NodeShellSession extends ShellSession {
protected nodeName: string;
protected podId: string
protected kc: KubeConfig
constructor(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster, nodeName: string) {
super(socket, pathToKubeconfig, cluster)
this.nodeName = nodeName
this.podId = `node-shell-${uuid()}`
this.kc = cluster.contextHandler.kc
}
public async open() {
const shell = await this.kubectl.kubectlPath()
let args = []
if (this.createNodeShellPod(this.podId, this.nodeName)) {
await this.waitForRunningPod(this.podId).catch((error) => {
this.exit(1001)
})
}
args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"]
const shellEnv = await this.getCachedShellEnv()
this.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || shellEnv["HOME"],
env: shellEnv,
name: "xterm-256color",
rows: 30,
});
this.running = true;
this.pipeStdout()
this.pipeStdin()
this.closeWebsocketOnProcessExit()
this.exitProcessOnWebsocketClose()
tracker.event("node-shell", "open")
}
protected exit(code = 1000) {
if (this.podId) {
this.deleteNodeShellPod()
}
super.exit(code)
}
protected async createNodeShellPod(podId: string, nodeName: string) {
const kc = this.getKubeConfig();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
const pod = {
metadata: {
name: podId,
namespace: "kube-system"
},
spec: {
restartPolicy: "Never",
terminationGracePeriodSeconds: 0,
hostPID: true,
hostIPC: true,
hostNetwork: true,
tolerations: [{
operator: "Exists"
}],
containers: [{
name: "shell",
image: "docker.io/alpine:3.9",
securityContext: {
privileged: true,
},
command: ["nsenter"],
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
}],
nodeSelector: {
"kubernetes.io/hostname": nodeName
}
}
} as k8s.V1Pod;
await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => {
logger.error(error)
return false
})
return true
}
protected getKubeConfig() {
if (this.kc) {
return this.kc
}
this.kc = new k8s.KubeConfig();
this.kc.loadFromFile(this.kubeconfigPath)
return this.kc
}
protected waitForRunningPod(podId: string) {
return new Promise<boolean>(async (resolve, reject) => {
const kc = this.getKubeConfig();
const watch = new k8s.Watch(kc);
const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {},
// callback is called for each received object.
(_type, obj) => {
if (obj.metadata.name == podId && obj.status.phase === "Running") {
resolve(true)
}
},
// done callback is called if the watch terminates normally
(err) => {
logger.error(err)
reject(false)
}
);
setTimeout(() => { req.abort(); reject(false); }, 120 * 1000);
})
}
protected deleteNodeShellPod() {
const kc = this.getKubeConfig();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
k8sApi.deleteNamespacedPod(this.podId, "kube-system")
}
}
export async function open(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster, nodeName?: string): Promise<ShellSession> {
return new Promise(async(resolve, reject) => {
let shell = null
if (nodeName) {
shell = new NodeShellSession(socket, pathToKubeconfig, cluster, nodeName)
}
else {
shell = new ShellSession(socket, pathToKubeconfig, cluster)
}
shell.open()
resolve(shell)
})
}
-30
View File
@@ -1,30 +0,0 @@
import logger from "./logger"
import { createServer } from "net"
import { AddressInfo } from "net"
const getNextAvailablePort = () => {
logger.debug("getNextAvailablePort() start")
const server = createServer()
server.unref()
return new Promise<number>((resolve, reject) =>
server
.on('error', (error: any) => reject(error))
.on('listening', () => {
logger.debug("*** server listening event ***")
const _port = (server.address() as AddressInfo).port
server.close(() => resolve(_port))
})
.listen({host: "127.0.0.1", port: 0}))
}
export const getFreePort = async () => {
logger.debug("getFreePort() start")
let freePort: number = null
try {
freePort = await getNextAvailablePort()
logger.debug("got port : " + freePort)
} catch(error) {
throw("getNextAvailablePort() threw: '" + error + "'")
}
return freePort
}
-29
View File
@@ -1,29 +0,0 @@
import { PrometheusLens } from "./lens"
import { CoreV1Api } from "@kubernetes/client-node"
import { PrometheusService } from "./provider-registry";
import logger from "../logger"
export class PrometheusHelm extends PrometheusLens {
id = "helm"
name = "Helm"
rateAccuracy = "5m"
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
const labelSelector = "app=prometheus,component=server,heritage=Helm"
try {
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector)
const service = serviceList.body.items[0]
if (!service) return
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
} catch(error) {
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`)
return
}
}
}
-83
View File
@@ -1,83 +0,0 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"
export class PrometheusLens implements PrometheusProvider {
id = "lens"
name = "Lens"
rateAccuracy = "1m"
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
try {
const resp = await client.readNamespacedService("prometheus", "lens-metrics")
const service = resp.body
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
} catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.toString()}`)
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
switch(opts.category) {
case 'cluster':
return {
memoryUsage: `
sum(
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
) by (kubernetes_name)
`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`),
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
cpuUsage: `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`,
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
podUsage: `sum(kubelet_running_pod_count{instance=~"${opts.nodes}"})`,
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`,
fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`
}
case 'nodes':
return {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`,
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`,
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)`
}
case 'pods':
return {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
}
case 'pvc':
return {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
}
case 'ingress':
const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`
return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
}
}
}
}
-91
View File
@@ -1,91 +0,0 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api, V1Service } from "@kubernetes/client-node";
import logger from "../logger";
export class PrometheusOperator implements PrometheusProvider {
rateAccuracy = "1m"
id = "operator"
name = "Prometheus Operator"
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
try {
let service: V1Service
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
if (!service) {
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector)
service = serviceList.body.items[0]
}
}
if (!service) return
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
} catch(error) {
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`)
return
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
switch(opts.category) {
case 'cluster':
return {
memoryUsage: `
sum(
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
) by (node)
`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`),
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`,
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
podUsage: `sum(kubelet_running_pod_count{node=~"${opts.nodes}"})`,
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`
}
case 'nodes':
return {
memoryUsage: `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`
}
case 'pods':
return {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
}
case 'pvc':
return {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
}
case 'ingress':
const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
}
}
}
}
-90
View File
@@ -1,90 +0,0 @@
import { CoreV1Api } from "@kubernetes/client-node"
export type PrometheusClusterQuery = {
memoryUsage: string;
memoryRequests: string;
memoryLimits: string;
memoryCapacity: string;
cpuUsage: string;
cpuRequests: string;
cpuLimits: string;
cpuCapacity: string;
podUsage: string;
podCapacity: string;
}
export type PrometheusNodeQuery = {
memoryUsage: string;
memoryCapacity: string;
cpuUsage: string;
cpuCapacity: string;
fsSize: string;
fsUsage: string;
}
export type PrometheusPodQuery = {
memoryUsage: string;
memoryRequests: string;
memoryLimits: string;
cpuUsage: string;
cpuRequests: string;
cpuLimits: string;
fsUsage: string;
networkReceive: string;
networkTransmit: string;
}
export type PrometheusPvcQuery = {
diskUsage: string;
diskCapacity: string;
}
export type PrometheusIngressQuery = {
bytesSentSuccess: string;
bytesSentFailure: string;
requestDurationSeconds: string;
responseDurationSeconds: string;
}
export type PrometheusQueryOpts = {
[key: string]: string | any;
};
export type PrometheusQuery = PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery
export type PrometheusService = {
id: string;
namespace: string;
service: string;
port: number;
}
export interface PrometheusProvider {
id: string;
name: string;
getQueries(opts: PrometheusQueryOpts): PrometheusQuery;
getPrometheusService(client: CoreV1Api): Promise<PrometheusService>;
}
export type PrometheusProviderList = {
[key: string]: PrometheusProvider;
}
export class PrometheusProviderRegistry {
private static prometheusProviders: PrometheusProviderList = {}
static getProvider(type: string): PrometheusProvider {
if (!this.prometheusProviders[type]) {
throw "Unknown Prometheus provider";
}
return this.prometheusProviders[type]
}
static registerProvider(key: string, provider: PrometheusProvider) {
this.prometheusProviders[key] = provider
}
static getProviders(): PrometheusProvider[] {
return Object.values(this.prometheusProviders)
}
}
-4
View File
@@ -1,4 +0,0 @@
import * as util from "util"
import { exec } from "child_process";
export const promiseExec = util.promisify(exec)
-18
View File
@@ -1,18 +0,0 @@
import { app } from "electron"
const switchValue = app.commandLine.getSwitchValue("proxy-server")
export function mangleProxyEnv() {
let httpsProxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || ""
delete process.env.HTTPS_PROXY
delete process.env.HTTP_PROXY
if (switchValue !== "") {
httpsProxy = switchValue
}
if (httpsProxy !== "") {
process.env.APP_HTTPS_PROXY = httpsProxy
}
}
-178
View File
@@ -1,178 +0,0 @@
import * as http from "http";
import * as httpProxy from "http-proxy";
import { Socket } from "net";
import * as url from "url";
import * as WebSocket from "ws"
import { ContextHandler } from "./context-handler";
import logger from "./logger"
import * as shell from "./node-shell-session"
import { ClusterManager } from "./cluster-manager"
import { Router } from "./router"
export class LensProxy {
public static readonly localShellSessions = true
public port: number;
protected clusterUrl: url.UrlWithStringQuery
protected clusterManager: ClusterManager
protected retryCounters: Map<string, number> = new Map()
protected router: Router
constructor(port: number, clusterManager: ClusterManager) {
this.port = port
this.clusterManager = clusterManager
this.router = new Router()
}
public run() {
const proxyServer = this.buildProxyServer();
proxyServer.listen(this.port, "127.0.0.1")
}
protected buildProxyServer() {
const proxy = this.createProxy();
const proxyServer = http.createServer(function(req: http.IncomingMessage, res: http.ServerResponse) {
this.handleRequest(proxy, req, res);
}.bind(this));
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
this.handleWsUpgrade(req, socket, head)
}.bind(this));
proxyServer.on("error", (err) => {
logger.error(err)
});
return proxyServer;
}
protected createProxy() {
const proxy = httpProxy.createProxyServer();
proxy.on("proxyRes", (proxyRes, req, res) => {
if (proxyRes.statusCode === 502) {
const cluster = this.clusterManager.getClusterForRequest(req)
if (cluster && cluster.contextHandler.proxyServerError()) {
res.writeHead(proxyRes.statusCode, {
"Content-Type": "text/plain"
})
res.end(cluster.contextHandler.proxyServerError().toString())
return
}
}
if (req.method !== "GET") {
return
}
const key = `${req.headers.host}${req.url}`
if (this.retryCounters.has(key)) {
logger.debug("Resetting proxy retry cache for url: " + key)
this.retryCounters.delete(key)
}
})
proxy.on("error", (error, req, res, target) => {
if (target) {
logger.debug("Failed proxy to target: " + JSON.stringify(target))
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
const retryCounterKey = `${req.headers.host}${req.url}`
const retryCount = this.retryCounters.get(retryCounterKey) || 0
if (retryCount < 20) {
logger.debug("Retrying proxy request to url: " + retryCounterKey)
setTimeout(() => {
this.retryCounters.set(retryCounterKey, retryCount + 1)
this.handleRequest(proxy, req, res)
}, (250 * retryCount))
}
}
}
res.writeHead(500, {
'Content-Type': 'text/plain'
})
res.end('Oops, something went wrong.')
})
return proxy;
}
protected createWsListener() {
const ws = new WebSocket.Server({ noServer: true})
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req)
const contextHandler = cluster.contextHandler
const nodeParam = this.getNodeParam(req.url)
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => {
return new Promise<boolean>(async (resolve, reject) => {
const shellSession = await shell.open(con, kubeconfigPath, cluster, nodeParam)
shellSession.on("exit", () => {
resolve(true)
})
})
})
}).bind(this))
return ws
}
protected getNodeParam(requestUrl: string) {
const reqUrl = url.parse(requestUrl, true)
const urlParams = reqUrl.query
let nodeParam: string = null
for (const [key, value] of Object.entries(urlParams)) {
if (key === "node") {
nodeParam = value.toString()
}
}
return nodeParam
}
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
if (req.url.startsWith("/api-kube/")) {
delete req.headers.authorization
req.url = req.url.replace("/api-kube", "")
const isWatchRequest = req.url.includes("watch=")
return await contextHandler.getApiTarget(isWatchRequest)
}
}
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = this.clusterManager.getClusterForRequest(req)
if (!cluster) {
logger.error("Got request to unknown cluster")
logger.debug(req.headers.host + req.url)
res.statusCode = 503
res.end()
return
}
const contextHandler = cluster.contextHandler
try {
contextHandler.applyHeaders(req)
} catch (error) {
res.statusCode = 503
res.end()
return
}
contextHandler.ensureServer().then(async () => {
const proxyTarget = await this.getProxyTarget(req, contextHandler)
if (proxyTarget) {
proxy.web(req, res, proxyTarget)
} else {
this.router.route(cluster, req, res)
}
})
}
protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) {
const wsServer = this.createWsListener();
const cluster = this.clusterManager.getClusterForRequest(req)
const contextHandler = cluster.contextHandler
contextHandler.applyHeaders(req);
wsServer.handleUpgrade(req, socket, head, (con) => {
wsServer.emit("connection", con, req);
});
}
}
export function listen(port: number, clusterManager: ClusterManager) {
const proxyServer = new LensProxy(port, clusterManager)
proxyServer.run();
return proxyServer;
}
-17
View File
@@ -1,17 +0,0 @@
import { LensApiRequest } from "./router"
import * as resourceApplier from "./resource-applier"
import { LensApi } from "./lens-api"
class ResourceApplierApi extends LensApi {
public async applyResource(request: LensApiRequest) {
const { response, cluster, payload } = request
try {
const resource = await resourceApplier.apply(cluster, cluster.kubeconfigPath(), payload)
this.respondJson(response, [resource], 200)
} catch(error) {
this.respondText(response, error, 422)
}
}
}
export const resourceApplierApi = new ResourceApplierApi()
-101
View File
@@ -1,101 +0,0 @@
import { exec } from "child_process";
import * as fs from "fs";
import * as yaml from "js-yaml";
import * as path from "path";
import * as tempy from "tempy";
import logger from "./logger"
import { Cluster } from "./cluster";
import { tracker } from "./tracker";
type KubeObject = {
status: {};
metadata?: {
resourceVersion: number;
annotations?: {
"kubectl.kubernetes.io/last-applied-configuration": string;
};
};
}
export class ResourceApplier {
protected kubeconfigPath: string;
protected cluster: Cluster
constructor(cluster: Cluster, pathToKubeconfig: string) {
this.kubeconfigPath = pathToKubeconfig
this.cluster = cluster
}
public async apply(resource: any): Promise<string> {
this.sanitizeObject((resource as KubeObject))
try {
tracker.event("resource", "apply")
return await this.kubectlApply(yaml.safeDump(resource))
} catch(error) {
throw (error)
}
}
protected async kubectlApply(content: string): Promise<string> {
const kubectl = await this.cluster.kubeCtl.kubectlPath()
return new Promise<string>((resolve, reject) => {
const fileName = tempy.file({name: "resource.yaml"})
fs.writeFileSync(fileName, content)
const cmd = `"${kubectl}" apply --kubeconfig ${this.kubeconfigPath} -o json -f ${fileName}`
logger.debug("shooting manifests with: " + cmd);
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env)
const httpsProxy = this.cluster.preferences?.httpsProxy
if (httpsProxy) {
execEnv["HTTPS_PROXY"] = httpsProxy
}
exec(cmd, { env: execEnv },
(error, stdout, stderr) => {
if (stderr != "") {
fs.unlinkSync(fileName)
reject(stderr)
return
}
fs.unlinkSync(fileName)
resolve(JSON.parse(stdout))
})
})
}
public async kubectlApplyAll(resources: string[]): Promise<string> {
const kubectl = await this.cluster.kubeCtl.kubectlPath()
return new Promise<string>((resolve, reject) => {
const tmpDir = tempy.directory()
// Dump each resource into tmpDir
for (const i in resources) {
fs.writeFileSync(path.join(tmpDir, `${i}.yaml`), resources[i])
}
const cmd = `"${kubectl}" apply --kubeconfig ${this.kubeconfigPath} -o json -f ${tmpDir}`
console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout, stderr) => {
if(error) {
reject("Error applying manifests:" + error);
}
if (stderr != "") {
reject(stderr)
return
}
resolve(stdout)
})
})
}
protected sanitizeObject(resource: KubeObject) {
delete resource['status']
if (resource['metadata']) {
if (resource['metadata']['annotations'] && resource['metadata']['annotations']['kubectl.kubernetes.io/last-applied-configuration']) {
delete resource['metadata']['annotations']['kubectl.kubernetes.io/last-applied-configuration']
}
delete resource['metadata']['resourceVersion']
}
}
}
export async function apply(cluster: Cluster, pathToKubeconfig: string, resource: any) {
const resourceApplier = new ResourceApplier(cluster, pathToKubeconfig)
return await resourceApplier.apply(resource)
}
-143
View File
@@ -1,143 +0,0 @@
import * as http from "http"
import * as path from "path"
import { Cluster } from "./cluster"
import { configRoute } from "./routes/config"
import { helmApi } from "./helm-api"
import { resourceApplierApi } from "./resource-applier-api"
import { kubeconfigRoute } from "./routes/kubeconfig"
import { metricsRoute } from "./routes/metrics"
import { watchRoute } from "./routes/watch"
import { portForwardRoute } from "./routes/port-forward"
import { readFile } from "fs"
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Call = require('@hapi/call');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Subtext = require('@hapi/subtext');
declare const __static: string;
const assetsPath = path.join(__static, "build/client")
const mimeTypes: {[key: string]: string} = {
"html": "text/html",
"txt": "text/plain",
"css": "text/css",
"gif": "image/gif",
"jpg": "image/jpeg",
"png": "image/png",
"svg": "image/svg+xml",
"js": "application/javascript",
"woff2": "font/woff2",
"ttf": "font/ttf"
};
interface RouteParams {
[key: string]: string | undefined;
}
export type LensApiRequest = {
cluster: Cluster;
payload: any;
raw: {
req: http.IncomingMessage;
};
params: RouteParams;
response: http.ServerResponse;
query: URLSearchParams;
path: string;
}
export class Router {
protected router: any
public constructor() {
this.router = new Call.Router();
this.addRoutes()
}
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) {
const url = new URL(req.url, "http://localhost")
const path = url.pathname
const method = req.method.toLowerCase()
const matchingRoute = this.router.route(method, path)
if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true
const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params })
await matchingRoute.route(request)
return true
} else {
return false
}
}
protected async getRequest(opts: { req: http.IncomingMessage; res: http.ServerResponse; cluster: Cluster; url: URL; params: RouteParams }) {
const { req, res, url, cluster, params } = opts
const { payload } = await Subtext.parse(req, null, { parse: true, output: 'data' });
const request: LensApiRequest = {
cluster: cluster,
path: url.pathname,
raw: {
req: req,
},
response: res,
query: url.searchParams,
payload: payload,
params: params
}
return request
}
protected handleStaticFile(file: string, response: http.ServerResponse) {
const asset = path.join(assetsPath, file)
readFile(asset, (err, data) => {
if (err) {
// default to index.html so that react routes work when page is refreshed
this.handleStaticFile("index.html", response)
} else {
const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain";
response.setHeader("Content-Type", type);
response.write(data)
response.end()
}
})
}
protected addRoutes() {
// Static assets
this.router.add({ method: 'get', path: '/{path*}' }, (request: LensApiRequest) => {
const { response, params } = request
const file = params.path || "/index.html"
this.handleStaticFile(file, response)
})
this.router.add({ method: 'get', path: '/api/config' }, configRoute.routeConfig.bind(configRoute))
this.router.add({ method: 'get', path: '/api/kubeconfig/service-account/{namespace}/{account}' }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
// Watch API
this.router.add({ method: 'get', path: '/api/watch' }, watchRoute.routeWatch.bind(watchRoute))
// Metrics API
this.router.add({ method: 'post', path: '/api/metrics' }, metricsRoute.routeMetrics.bind(metricsRoute))
// Port-forward API
this.router.add({ method: 'post', path: '/api/services/{namespace}/{service}/port-forward/{port}' }, portForwardRoute.routeServicePortForward.bind(portForwardRoute))
// Helm API
this.router.add({ method: 'get', path: '/api-helm/v2/charts' }, helmApi.listCharts.bind(helmApi))
this.router.add({ method: 'get', path: '/api-helm/v2/charts/{repo}/{chart}' }, helmApi.getChart.bind(helmApi))
this.router.add({ method: 'get', path: '/api-helm/v2/charts/{repo}/{chart}/values' }, helmApi.getChartValues.bind(helmApi))
this.router.add({ method: 'post', path: '/api-helm/v2/releases' }, helmApi.installChart.bind(helmApi))
this.router.add({ method: 'put', path: '/api-helm/v2/releases/{namespace}/{release}' }, helmApi.updateRelease.bind(helmApi))
this.router.add({ method: 'put', path: '/api-helm/v2/releases/{namespace}/{release}/rollback' }, helmApi.rollbackRelease.bind(helmApi))
this.router.add({ method: 'get', path: '/api-helm/v2/releases/{namespace?}' }, helmApi.listReleases.bind(helmApi))
this.router.add({ method: 'get', path: '/api-helm/v2/releases/{namespace}/{release}' }, helmApi.getRelease.bind(helmApi))
this.router.add({ method: 'get', path: '/api-helm/v2/releases/{namespace}/{release}/values' }, helmApi.getReleaseValues.bind(helmApi))
this.router.add({ method: 'get', path: '/api-helm/v2/releases/{namespace}/{release}/history' }, helmApi.getReleaseHistory.bind(helmApi))
this.router.add({ method: 'delete', path: '/api-helm/v2/releases/{namespace}/{release}' }, helmApi.deleteRelease.bind(helmApi))
// Resource Applier API
this.router.add({ method: 'post', path: '/api-resource/stack' }, resourceApplierApi.applyResource.bind(resourceApplierApi))
}
}
-96
View File
@@ -1,96 +0,0 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { userStore } from "../../common/user-store"
import { getAppVersion } from "../../common/app-utils"
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
import { Cluster } from "../cluster"
// TODO: auto-populate all resources dynamically
const apiResources = [
{ resource: "configmaps" },
{ resource: "cronjobs", group: "batch" },
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
{ resource: "daemonsets", group: "apps" },
{ resource: "deployments", group: "apps" },
{ resource: "endpoints" },
{ resource: "events" },
{ resource: "horizontalpodautoscalers" },
{ resource: "ingresses", group: "networking.k8s.io" },
{ resource: "jobs", group: "batch" },
{ resource: "namespaces" },
{ resource: "networkpolicies", group: "networking.k8s.io" },
{ resource: "nodes" },
{ resource: "persistentvolumes" },
{ resource: "pods" },
{ resource: "podsecuritypolicies" },
{ resource: "resourcequotas" },
{ resource: "secrets" },
{ resource: "services" },
{ resource: "statefulsets", group: "apps" },
{ resource: "storageclasses", group: "storage.k8s.io" },
]
async function getAllowedNamespaces(cluster: Cluster) {
const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api)
try {
const namespaceList = await api.listNamespace()
const nsAccessStatuses = await Promise.all(
namespaceList.body.items.map(ns => cluster.canI({
namespace: ns.metadata.name,
resource: "pods",
verb: "list",
}))
)
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name)
} catch(error) {
const kc = cluster.contextHandler.kc
const ctx = kc.getContextObject(kc.currentContext)
if (ctx.namespace) {
return [ctx.namespace]
} else {
return []
}
}
}
async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
try {
const resourceAccessStatuses = await Promise.all(
apiResources.map(apiResource => cluster.canI({
resource: apiResource.resource,
group: apiResource.group,
verb: "list",
namespace: namespaces[0]
}))
)
return apiResources
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
} catch(error) {
return []
}
}
class ConfigRoute extends LensApi {
public async routeConfig(request: LensApiRequest) {
const { params, response, cluster} = request
const namespaces = await getAllowedNamespaces(cluster)
const data = {
clusterName: cluster.contextName,
lensVersion: getAppVersion(),
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
kubeVersion: cluster.version,
chartsEnabled: true,
isClusterAdmin: cluster.isAdmin,
allowedResources: await getAllowedResources(cluster, namespaces),
allowedNamespaces: namespaces
};
this.respondJson(response, data)
}
}
export const configRoute = new ConfigRoute()
-58
View File
@@ -1,58 +0,0 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Cluster } from "../cluster"
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
const tokenData = new Buffer(secret.data["token"], "base64")
return {
'apiVersion': 'v1',
'kind': 'Config',
'clusters': [
{
'name': cluster.contextName,
'cluster': {
'server': cluster.contextHandler.kc.getCurrentCluster().server,
'certificate-authority-data': secret.data["ca.crt"]
}
}
],
'users': [
{
'name': username,
'user': {
'token': tokenData.toString("utf8"),
}
}
],
'contexts': [
{
'name': cluster.contextName,
'context': {
'user': username,
'cluster': cluster.contextName,
'namespace': secret.metadata.namespace,
}
}
],
'current-context': cluster.contextName
}
}
class KubeconfigRoute extends LensApi {
public async routeServiceAccountRoute(request: LensApiRequest) {
const { params, response, cluster} = request
const client = cluster.contextHandler.kc.makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace)
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata;
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
});
const data = generateKubeConfig(params.account, secret, cluster);
this.respondJson(response, data)
}
}
export const kubeconfigRoute = new KubeconfigRoute()
-91
View File
@@ -1,91 +0,0 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import * as requestPromise from "request-promise-native"
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry"
type MetricsQuery = string | string[] | {
[metricName: string]: string;
}
class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest) {
const { response, cluster} = request
const query: MetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
const headers = {
"Host": `${cluster.id}.localhost:${cluster.port}`,
"Content-type": "application/json",
}
const queryParams: MetricsQuery = {}
request.query.forEach((value: string, key: string) => {
queryParams[key] = value
})
let metricsUrl: string
let prometheusProvider: PrometheusProvider
try {
const prometheusPath = await cluster.contextHandler.getPrometheusPath()
metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
} catch {
this.respondJson(response, {})
return
}
// prometheus metrics loader
const attempts: { [query: string]: number } = {};
const maxAttempts = 5;
const loadMetrics = (orgQuery: string): Promise<any> => {
const query = orgQuery.trim()
const attempt = attempts[query] = (attempts[query] || 0) + 1;
return requestPromise(metricsUrl, {
resolveWithFullResponse: false,
headers: headers,
json: true,
qs: {
query: query,
...queryParams
}
}).catch(async (error) => {
if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
return loadMetrics(query);
}
return {
status: error.toString(),
data: {
result: []
}
}
})
};
// return data in same structure as query
let data: any;
if (typeof query === "string") {
data = await loadMetrics(query)
}
else if (Array.isArray(query)) {
data = await Promise.all(query.map(loadMetrics));
}
else {
data = {};
const result = await Promise.all(
Object.entries(query).map((queryEntry: any) => {
const queryName: string = queryEntry[0]
const queryOpts: PrometheusQueryOpts = queryEntry[1]
const queries = prometheusProvider.getQueries(queryOpts)
const q = queries[queryName as keyof (PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery)]
return loadMetrics(q)
})
);
Object.keys(query).forEach((metricName, index) => {
data[metricName] = result[index];
});
}
this.respondJson(response, data)
}
}
export const metricsRoute = new MetricsRoute()
-107
View File
@@ -1,107 +0,0 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { spawn, ChildProcessWithoutNullStreams } from "child_process"
import { bundledKubectl } from "../kubectl"
import { getFreePort } from "../port"
import { shell } from "electron"
import * as tcpPortUsed from "tcp-port-used"
import logger from "../logger"
class PortForward {
public static portForwards: PortForward[] = []
static getPortforward(forward: {clusterId: string; kind: string; name: string; namespace: string; port: string}) {
return PortForward.portForwards.find((pf) => {
return (
pf.clusterId == forward.clusterId &&
pf.kind == "service" &&
pf.name == forward.name &&
pf.namespace == forward.namespace &&
pf.port == forward.port
)
})
}
public clusterId: string
public process: ChildProcessWithoutNullStreams
public kubeConfig: string
public kind: string
public namespace: string
public name: string
public port: string
public localPort: number
constructor(obj: any) {
Object.assign(this, obj)
}
public async start() {
this.localPort = await getFreePort()
const kubectlBin = await bundledKubectl.kubectlPath()
const args = [
"--kubeconfig", this.kubeConfig,
"port-forward",
"-n", this.namespace,
`service/${this.name}`,
`${this.localPort}:${this.port}`
]
this.process = spawn(kubectlBin, args, {
env: process.env
})
PortForward.portForwards.push(this)
this.process.on("exit", () => {
const index = PortForward.portForwards.indexOf(this)
if (index > -1) {
PortForward.portForwards.splice(index, 1)
}
})
try {
await tcpPortUsed.waitUntilUsed(this.localPort, 500, 3000)
return true
} catch (error) {
this.process.kill()
return false
}
}
public open() {
shell.openExternal(`http://localhost:${this.localPort}`)
}
}
class PortForwardRoute extends LensApi {
public async routeServicePortForward(request: LensApiRequest) {
const { params, response, cluster} = request
let portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: "service", name: params.service,
namespace: params.namespace, port: params.port
})
if (!portForward) {
logger.info(`Creating a new port-forward ${params.namespace}/${params.service}:${params.port}`)
portForward = new PortForward({
clusterId: cluster.id,
kind: "service",
namespace: params.namespace,
name: params.service,
port: params.port,
kubeConfig: cluster.kubeconfigPath()
})
const started = await portForward.start()
if (!started) {
this.respondJson(response, {
message: "Failed to open port-forward"
}, 400)
return
}
}
portForward.open()
this.respondJson(response, {})
}
}
export const portForwardRoute = new PortForwardRoute()
-105
View File
@@ -1,105 +0,0 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
import { ServerResponse } from "http"
import { Request } from "request"
import logger from "../logger"
class ApiWatcher {
private apiUrl: string
private response: ServerResponse
private watchRequest: Request
private watch: Watch
private processor: NodeJS.Timeout
private eventBuffer: any[] = []
constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
this.apiUrl = apiUrl
this.watch = new Watch(kubeConfig)
this.response = response
}
public async start() {
if (this.processor) {
clearInterval(this.processor)
}
this.processor = setInterval(() => {
const events = this.eventBuffer.splice(0)
events.map(event => this.sendEvent(event))
this.response.flushHeaders()
}, 50)
this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this))
}
public stop() {
if (!this.watchRequest) { return }
if (this.processor) {
clearInterval(this.processor)
}
logger.debug("Stopping watcher for api: " + this.apiUrl)
this.watchRequest.abort()
}
private watchHandler(phase: string, obj: RuntimeRawExtension) {
this.eventBuffer.push({
type: phase,
object: obj
})
}
private doneHandler(error: Error) {
if (error) logger.warn("watch ended: " + error.toString())
this.sendEvent({
type: "STREAM_END",
url: this.apiUrl,
status: 410,
})
}
private sendEvent(evt: any) {
// convert to "text/event-stream" format
this.response.write(`data: ${JSON.stringify(evt)}\n\n`);
}
}
class WatchRoute extends LensApi {
public async routeWatch(request: LensApiRequest) {
const { params, response, cluster} = request
const apis: string[] = request.query.getAll("api")
const watchers: ApiWatcher[] = []
if (!apis.length) {
this.respondJson(response, {
message: "Empty request. Query params 'api' are not provided.",
example: "?api=/api/v1/pods&api=/api/v1/nodes",
}, 400)
return
}
response.setHeader("Content-Type", "text/event-stream")
response.setHeader("Cache-Control", "no-cache")
response.setHeader("Connection", "keep-alive")
apis.forEach(apiUrl => {
const watcher = new ApiWatcher(apiUrl, cluster.contextHandler.kc, response)
watcher.start()
watchers.push(watcher)
})
request.raw.req.on("close", () => {
logger.debug("Watch request closed")
watchers.map(watcher => watcher.stop())
})
request.raw.req.on("end", () => {
logger.debug("Watch request ended")
watchers.map(watcher => watcher.stop())
})
}
}
export const watchRoute = new WatchRoute()
-191
View File
@@ -1,191 +0,0 @@
import * as pty from "node-pty"
import * as WebSocket from "ws"
import { EventEmitter } from "events";
import * as path from "path"
import shellEnv = require("shell-env")
import { app } from "electron"
import { Kubectl } from "./kubectl"
import { tracker } from "./tracker"
import { Cluster, ClusterPreferences } from "./cluster"
import { helmCli } from "./helm-cli"
export class ShellSession extends EventEmitter {
static shellEnv: any
protected websocket: WebSocket
protected shellProcess: pty.IPty
protected kubeconfigPath: string
protected nodeShellPod: string;
protected kubectl: Kubectl;
protected kubectlBinDir: string;
protected helmBinDir: string;
protected preferences: ClusterPreferences;
protected running = false;
constructor(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster) {
super()
this.websocket = socket
this.kubeconfigPath = pathToKubeconfig
this.kubectl = new Kubectl(cluster.version)
this.preferences = cluster.preferences || {}
}
public async open() {
this.kubectlBinDir = await this.kubectl.binDir()
this.helmBinDir = helmCli.getBinaryDir()
const env = await this.getCachedShellEnv()
const shell = env.PTYSHELL
const args = await this.getShellArgs(shell)
this.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || env.HOME,
env: env,
name: "xterm-256color",
rows: 30,
});
this.running = true;
this.pipeStdout()
this.pipeStdin()
this.closeWebsocketOnProcessExit()
this.exitProcessOnWebsocketClose()
tracker.event("shell", "open")
}
protected cwd(): string {
if(!this.preferences || !this.preferences.terminalCWD || this.preferences.terminalCWD === "") {
return null
}
return this.preferences.terminalCWD
}
protected async getShellArgs(shell: string): Promise<Array<string>> {
switch(path.basename(shell)) {
case "powershell.exe":
return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.kubectlBinDir};${this.helmBinDir};$Env:PATH"}`]
break
case "bash":
return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')]
break
case "fish":
return ["--login", "--init-command", `export PATH="${this.kubectlBinDir}:${this.helmBinDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`]
break
case "zsh":
return ["--login"]
default:
return []
}
}
protected async getCachedShellEnv() {
let env: any
if (!ShellSession.shellEnv) {
env = await this.getShellEnv()
ShellSession.shellEnv = env
} else {
env = ShellSession.shellEnv
// refresh env in the background
this.getShellEnv().then((shellEnv: any) => {
ShellSession.shellEnv = shellEnv
})
}
return env
}
protected async getShellEnv() {
const env = JSON.parse(JSON.stringify(await shellEnv()))
const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter)
if(process.platform === "win32") {
env["SystemRoot"] = process.env.SystemRoot
env["PTYSHELL"] = "powershell.exe"
env["PATH"] = pathStr
} else if(typeof(process.env.SHELL) != "undefined") {
env["PTYSHELL"] = process.env.SHELL
env["PATH"] = pathStr
} else {
env["PTYSHELL"] = "" // blank runs the system default shell
}
if(path.basename(env["PTYSHELL"]) === "zsh") {
env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME
env["ZDOTDIR"] = this.kubectlBinDir
}
env["PTYPID"] = process.pid.toString()
env["KUBECONFIG"] = this.kubeconfigPath
env["TERM_PROGRAM"] = app.getName()
env["TERM_PROGRAM_VERSION"] = app.getVersion()
if (this.preferences.httpsProxy) {
env["HTTPS_PROXY"] = this.preferences.httpsProxy
}
if (env.DEBUG) { // do not pass debug option to bash
delete env["DEBUG"]
}
return(env)
}
protected pipeStdout() {
// send shell output to websocket
this.shellProcess.on("data", ((data: string) => {
this.sendResponse(data)
}).bind(this));
}
protected pipeStdin() {
// write websocket messages to shellProcess
this.websocket.on("message", function(data: string) {
if (!this.running) { return }
const message = Buffer.from(data.slice(1, data.length), "base64").toString()
switch (data[0]) {
case "0":
this.shellProcess.write(message)
break;
case "4":
const resizeMsgObj = JSON.parse(message)
this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"])
break;
case "9":
this.token = message
this.emit('newToken', this.token)
break;
}
}.bind(this))
}
protected exit(code = 1000) {
this.websocket.close(code)
this.emit('exit')
}
protected closeWebsocketOnProcessExit() {
this.shellProcess.on("exit", (code) => {
this.running = false
let timeout = 0
if (code > 0) {
this.sendResponse("Terminal will auto-close in 15 seconds ...")
timeout = 15*1000
}
setTimeout(() => {
this.exit()
}, timeout)
});
}
protected exitProcessOnWebsocketClose() {
this.websocket.on("close", () => {
if (this.shellProcess) {
this.shellProcess.kill();
}
})
}
protected sendResponse(msg: string) {
this.websocket.send("1" + Buffer.from(msg).toString("base64"))
}
}
-18
View File
@@ -1,18 +0,0 @@
import shellEnv = require("shell-env")
import logger from "./logger"
export async function shellSync() {
const env = await shellEnv()
// Overwrite PATH on darwin
if (process.env.NODE_ENV === "production" && process.platform === "darwin") {
process.env["PATH"] = env.PATH
}
let key = null
for(key in env) {
if(!env.hasOwnProperty(key) || process.env[key]) continue // skip existing and prototype keys
logger.debug("Imported " + key + " from login shell to process environment")
process.env[key] = env[key]
}
}
-4
View File
@@ -1,4 +0,0 @@
import { Tracker } from "../common/tracker"
import { app, remote } from "electron"
export const tracker = new Tracker(app || remote.app);
-7
View File
@@ -1,7 +0,0 @@
import { webContents } from "electron"
/**
* Helper to find the correct web contents handle for main window
*/
export function findMainWebContents() {
return webContents.getAllWebContents().find(w => w.getType() === "window");
}
-91
View File
@@ -1,91 +0,0 @@
import { BrowserWindow, shell, Menu } from "electron"
import { PromiseIpc } from "electron-promise-ipc"
import * as windowStateKeeper from "electron-window-state"
import * as path from "path"
import { tracker } from "./tracker";
declare const __static: string;
export class WindowManager {
public mainWindow: BrowserWindow = null;
public splashWindow: BrowserWindow = null;
protected promiseIpc: any
protected windowState: windowStateKeeper.State;
constructor(showSplash = true) {
this.promiseIpc = new PromiseIpc({ timeout: 2000 })
// Manage main window size&position with persistence
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
this.splashWindow = new BrowserWindow({
width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false
})
if (showSplash) {
this.splashWindow.loadFile(path.join(__static, "/splash.html"))
this.splashWindow.show()
}
this.mainWindow = new BrowserWindow({
x: this.windowState.x,
y: this.windowState.y,
width: this.windowState.width,
height: this.windowState.height,
backgroundColor: "#1e2124",
titleBarStyle: "hidden",
webPreferences: {
nodeIntegration: true,
webviewTag: true
},
show: false
});
// Hook window state manager into window lifecycle
this.windowState.manage(this.mainWindow);
// handle close event
this.mainWindow.on("close", () => {
this.mainWindow = null;
})
// handle external links
this.mainWindow.webContents.on("will-navigate", (event, link) => {
if (link.startsWith("http://localhost")) {
return;
}
event.preventDefault();
shell.openExternal(link);
})
// handle developer console
if (process.env.NODE_ENV !== "production") {
this.mainWindow.webContents.on("devtools-opened", () => {
if (this.mainWindow.getBrowserView()) {
this.mainWindow.getBrowserView().webContents.openDevTools({mode: "detach"})
}
})
}
this.mainWindow.on("focus", () => {
tracker.event("app", "focus")
})
}
public showMain(url: string) {
this.mainWindow.loadURL( url ).then(() => {
this.splashWindow.hide()
this.splashWindow.loadURL("data:text/html;charset=utf-8,").then(() => {
this.splashWindow.close()
this.mainWindow.show()
})
})
}
}
-37
View File
@@ -1,37 +0,0 @@
<template>
<div id="app">
<div class="draggable-top" />
<div id="lens-container" />
<div class="main-view" :class="{ 'menu-visible': isMenuVisible }">
<main-menu v-if="isMenuVisible" />
<router-view />
</div>
<bottom-bar v-if="isMenuVisible" />
</div>
</template>
<script>
import MainMenu from "@/components/MainMenu/MainMenu";
import BottomBar from "@/components/BottomBar/BottomBar";
export default {
name: 'Lens',
components: {
BottomBar,
MainMenu
},
computed:{
isMenuVisible: function(){ return this.$store.getters.isMenuVisible }
}
}
</script>
<style>
#lens-container {
position: absolute;
top: 0;
left: 70px;
right: 0;
height: 100%;
z-index: 100;
display: none;
}
</style>
-204
View File
@@ -1,204 +0,0 @@
@import 'custom.scss';
@import '~bootstrap/scss/bootstrap';
@import '~bootstrap-vue/src/index.scss';
@import 'fonts.scss';
html, body{
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: $lens-main-bg;
color: $lens-text-color;
font-size: 14px; // font size that is used also on lens UI
-webkit-font-smoothing: antialiased;
}
body{
font-family: 'Roboto', sans-serif;
}
pre {
color: $lens-text-color-light;
}
#app{
width: 100%;
height: 100%;
& > .main-view{
width: 100%;
height: 100%;
& > .content{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
}
&.menu-visible > .content{
left: 70px;
bottom: 20px;
}
}
select {
appearance: none;
border: 1px solid $lens-pane-bg;
background-color: $lens-menu-bg;
color: $lens-text-color-light;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAKxWlDQ1BEaXNwbGF5AABIiZWXd1BT+RbHv/emEhJaAgJSQm+CFOkivQSQ3m2EhBJKjCFg7yyuYEVFBCu4KDZcCyBrQSzYFnsvi7LY10VdbKi8P3iEfW/ezJs9M3fuZ75zft9zzm/uH+cCHKZQJssnNYACqUIeFxrAT0lN4zO6QQUTbLhCRSgqlPnHxEQCwND7b0EAH26BAIDr9kKZLB//LDTFmYUigIgBkCEuFBUAxCGAeCOSyRUAZTsAs+kKmQKgnAPAk6ekpgGUBwB42YPcC4CXkZKaBlCpAHjyhLhAgKoLMNlCoTwbYJsD4BeLshUAOwiAo1QskQJsBYBxohyhGGAfAjCqoGCqGGB3AbDO+JtP9n94Zig9hcJsJQ/OAgBgBkkKZfnCmf/wOv5/FOQXDdWwBMDOkYfFAdABiDt5UyOULM2Iih5iiRgY4pyisMQhFhUGpg2xWBgUoTybHxU5xFmSEIHSRyFIGOLMwuD4IZZPjVPWypIH+g+xUD5ctygvUannZAqU/rNyEpKHuFiSFDXEhXnxEcM5gUpdXhSn7D9TGhowXDdEOXtB4d/mlQiUZxU5CWHK2YXD/WdK/Yc9C1OUvYkzg4KHcxKV+TJFgLKWLD9GmZ+ZH6rUC4vjlWcV8oThfEWM8g5zheExQwx/xCMR8eAjBgFwhgfcEQsoMmcoACBwqmymXJKdo+D7y2T5mXyBVOQwiu/s6OQJpKSm8Qc/ifd3QAAgdJjDWr424GkBkDXDWsYroGU5oD4wrFndBNSigLYKUZG8eFCjAgANLKiDBz0YwQzWsIcz3OANPwQjHNFIQComQ4QcFECO6ZiDhShFOVZhHaqxBXXYib04gGYcxUmcxUVcwU3cRxd68Aq9+IB+giAYBIfgEnqEMWFB2BHOhAcxjggmIok4IpVIJ7IJKVFEzCEWE+VEBVFNbCMaiJ+JI8RJ4jxxlbhLPCZeEO+ILySFZJM80pC0JEeTHqQ/GUEmkJPIbHIaOYssIVeQVWQtuYdsIk+SF8mbZBf5iuyjgKJK0aGYUOwpHpRASjQljZJFkVPmUcoolZRayj5KK6WDcp3SRXlN+UylU7lUPtWe6k0NoyZSRdRp1HnUZdRq6k5qE/U09Tr1MbWX+p3GoRnQ7GheNAEthZZNm04rpVXS6mmHaWdoN2k9tA90Ol2HbkV3p4fRU+m59Nn0ZfRN9EZ6G/0qvZvex2Aw9Bh2DB9GNEPIUDBKGRsYexgnGNcYPYxPTFWmMdOZGcJMY0qZi5iVzF3M48xrzGfMfhUNFQsVL5VoFbHKTJWVKttVWlUuq/So9LM0WVYsH1YCK5e1kFXF2sc6w3rAeq+qqmqq6qkaqypRXaBapbpf9ZzqY9XPbC22LTuQPZFdxF7B3sFuY99lv+dwOJYcP04aR8FZwWngnOI84nxS46o5qAnUxGrz1WrUmtSuqb1RV1G3UPdXn6w+S71S/aD6ZfXXGioalhqBGkKNeRo1Gkc0bmv0aXI1nTSjNQs0l2nu0jyv+VyLoWWpFawl1irRqtM6pdXNpXDNuIFcEXcxdzv3DLeHR+dZ8QS8XF45by+vk9erraU9RjtJe4Z2jfYx7S4dio6ljkAnX2elzgGdWzpfRhiO8B+ROWLpiH0jro34qDtS1083U7dMt1H3pu4XPb5esF6e3mq9Zr2H+lR9W/1Y/en6m/XP6L8eyRvpPVI0smzkgZH3DEgDW4M4g9kGdQaXDPoMjQxDDWWGGwxPGb420jHyM8o1Wmt03OiFMdd4nLHEeK3xCeOXfG2+Pz+fX8U/ze81MTAJMyky2WbSadJvamWaaLrItNH0oRnLzMMsy2ytWbtZr7mx+XjzOea7ze9ZqFh4WORYrLfosPhoaWWZbLnEstnyuZWulcBqltVuqwfWHGtf62nWtdY3bOg2HjZ5NptsrtiStq62ObY1tpftSDs3O4ndJruro2ijPEdJR9WOum3Ptve3L7bfbf/YQcch0mGRQ7PDm9Hmo9NGrx7dMfq7o6tjvuN2x/tOWk7hToucWp3eOds6i5xrnG+4cFxCXOa7tLi8HWM3JnPM5jF3XLmu412XuLa7fnNzd5O77XN74W7unu6+0f22B88jxmOZxzlPmmeA53zPo56fvdy8FF4HvP70tvfO897l/Xys1djMsdvHdvuY+gh9tvl0jeOPSx+3dVyXr4mv0LfW94mfmZ/Yr97vmb+Nf67/Hv83AY4B8oDDAR8DvQLnBrYFUYJCg8qCOoO1ghODq4MfhZiGZIfsDukNdQ2dHdoWRguLCFsddltgKBAJGgS94e7hc8NPR7Aj4iOqI55E2kbKI1vHk+PDx68Z/yDKIkoa1RyNaEH0muiHMVYx02J+iaXHxsTWxD6Nc4qbE9cRz42fEr8r/kNCQMLKhPuJ1olFie1J6kkTkxqSPiYHJVckd6WMTpmbcjFVP1WS2pLGSEtKq0/rmxA8Yd2EnomuE0sn3ppkNWnGpPOT9SfnTz42RX2KcMrBdFp6cvqu9K/CaGGtsC9DkLExo1cUKFoveiX2E68Vv8j0yazIfJblk1WR9TzbJ3tN9osc35zKnNeSQEm15G1uWO6W3I950Xk78gbyk/MbC5gF6QVHpFrSPOnpqUZTZ0y9KrOTlcq6pnlNWzetVx4hry8kCicVtih4CpniUpF10Q9Fj4vHFdcUf5qeNP3gDM0Z0hmXZtrOXDrz2ayQWT/Nps4WzW6fYzJn4ZzHc/3nbptHzMuY1z7fbH7J/J4FoQt2LmQtzFv46yLHRRWL/lqcvLi1xLBkQUn3D6E/7C5VK5WX3l7ivWTLj9QfJT92LnVZumHp9zJx2YVyx/LK8q/LRMsuLHdaXrV8YEXWis6Vbis3r6Kvkq66tdp39c4KzYpZFd1rxq9pWstfW7b2r3VT1p2vHFO5ZT1rfdH6rqrIqpYN5htWbfhanVN9syagpnGjwcalGz9uEm+6ttlv874thlvKt3zZKtl6Z1votqZay9rKOnpdcd3T7UnbO37y+KmhXr++vP7bDumOrp1xO083uDc07DLYtXI3ubto94s9E/dc2Ru0t2Wf/b5tjTqN5fuxv2j/y5/Tf751IOJA+0GPg/sOWRzaeJh7uKyJaJrZ1Nuc09zVktpy9Uj4kfZW79bDvzj8suOoydGaY9rHVh5nHS85PnBi1om+Nlnb65PZJ7vbp7TfP5Vy6sbp2NOdZyLOnDsbcvZUh3/HiXM+546e9zp/5ILHheaLbhebLrleOvyr66+HO906my67X2654nml9erYq8ev+V47eT3o+tkbghsXb0bdvHor8dad2xNvd90R33l+N//u23vF9/rvL3hAe1D2UONh5SODR7W/2fzW2OXWdexx0ONLT+Kf3O8Wdb/6vfD3rz0lTzlPK58ZP2t47vz86IuQF1deTnjZ80r2qv916R+af2x8Y/3m0J9+f17qTenteSt/O/Bu2Xu99zv+GvNXe19M36MPBR/6P5Z90vu087PH544vyV+e9U//yvha9c3mW+v3iO8PBgoGBmRCuRAAQAFAZmUB73YAnFSAewVgTRjcqQEAxOB/ADC4g/xvHty7AQBuQB2AZD8gug3YCsBqAcBpA2L8gAQ/kC4uyuffUZjl4jzopdoM0CoHBt4nAwwb4NvtgYH+5oGBb/UA5R7Q9mFwlwcAqgSwrgOxcPN113tn8F/xL2AUEjlEqYDhAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF9mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDAgNzkuMTYwNDUxLCAyMDE3LzA1LzA2LTAxOjA4OjIxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTExLTA1VDE1OjUyOjMzKzAyOjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUxMTI1YTgyLWY2YmUtNDY1ZC05NDAyLTk0NWNhZGFjYTkzZSIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjJiNjlhZDExLTI0M2MtMzk0Ny05NDM3LWNiZjkzNTM3ODFiYyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSLine truncated
background-repeat: no-repeat, repeat;
background-position: right .7em top 50%, 0 0;
background-size: .65em auto, 100%;
}
}
.draggable-top{
-webkit-app-region: drag;
left: 0;
top: 0;
position: absolute;
height: 20px;
width: 100%;
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 12px $lens-pane-bg;
}
::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 12px $lens-pane-bg;
}
h1, h2, h3, h4, h5, h6{
color: #fff;
font-size: 20.8px;
font-weight: 300;
}
.table{
color: $lens-text-color-light;
th{
border-top: 1px solid #353a3e;
font-weight: 300;
color: $lens-text-color;
}
td{
border-top: 1px solid #353a3e;
}
tr:first-child{
th, td{
border-top: none;
}
}
}
.card{
background: $lens-pane-bg;
}
.help{
border-left: 1px solid #353a3e;
padding-top: 20px;
&:first-child{
padding-top: 0;
}
h3{
padding: 0.75rem 0 0.75rem 0;
}
}
.tooltip-inner {
font-size: 12px;
}
.editor {
height: 400px;
font-size: 12px;
pre {
background-color: #1E1E1E;
}
}
.aligner-center-center{
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.wrapper{
width:100%;
height:100%;
display:flex;
justify-content:center;
align-items:center;
.error{
text-align: center;
i {
color: #dc3545;
font-size: 100px;
}
}
}
.btn {
line-height: 1.3;
}
// Force BS modals to use main app font etc.
.modal-open{
@extend #app
}
.popover {
background-color: $lens-menu-hl;
select {
appearance: none;
border: 1px solid $lens-pane-bg;
background-color: $lens-menu-bg;
color: $lens-text-color-light;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAKxWlDQ1BEaXNwbGF5AABIiZWXd1BT+RbHv/emEhJaAgJSQm+CFOkivQSQ3m2EhBJKjCFg7yyuYEVFBCu4KDZcCyBrQSzYFnsvi7LY10VdbKi8P3iEfW/ezJs9M3fuZ75zft9zzm/uH+cCHKZQJssnNYACqUIeFxrAT0lN4zO6QQUTbLhCRSgqlPnHxEQCwND7b0EAH26BAIDr9kKZLB//LDTFmYUigIgBkCEuFBUAxCGAeCOSyRUAZTsAs+kKmQKgnAPAk6ekpgGUBwB42YPcC4CXkZKaBlCpAHjyhLhAgKoLMNlCoTwbYJsD4BeLshUAOwiAo1QskQJsBYBxohyhGGAfAjCqoGCqGGB3AbDO+JtP9n94Zig9hcJsJQ/OAgBgBkkKZfnCmf/wOv5/FOQXDdWwBMDOkYfFAdABiDt5UyOULM2Iih5iiRgY4pyisMQhFhUGpg2xWBgUoTybHxU5xFmSEIHSRyFIGOLMwuD4IZZPjVPWypIH+g+xUD5ctygvUannZAqU/rNyEpKHuFiSFDXEhXnxEcM5gUpdXhSn7D9TGhowXDdEOXtB4d/mlQiUZxU5CWHK2YXD/WdK/Yc9C1OUvYkzg4KHcxKV+TJFgLKWLD9GmZ+ZH6rUC4vjlWcV8oThfEWM8g5zheExQwx/xCMR8eAjBgFwhgfcEQsoMmcoACBwqmymXJKdo+D7y2T5mXyBVOQwiu/s6OQJpKSm8Qc/ifd3QAAgdJjDWr424GkBkDXDWsYroGU5oD4wrFndBNSigLYKUZG8eFCjAgANLKiDBz0YwQzWsIcz3OANPwQjHNFIQComQ4QcFECO6ZiDhShFOVZhHaqxBXXYib04gGYcxUmcxUVcwU3cRxd68Aq9+IB+giAYBIfgEnqEMWFB2BHOhAcxjggmIok4IpVIJ7IJKVFEzCEWE+VEBVFNbCMaiJ+JI8RJ4jxxlbhLPCZeEO+ILySFZJM80pC0JEeTHqQ/GUEmkJPIbHIaOYssIVeQVWQtuYdsIk+SF8mbZBf5iuyjgKJK0aGYUOwpHpRASjQljZJFkVPmUcoolZRayj5KK6WDcp3SRXlN+UylU7lUPtWe6k0NoyZSRdRp1HnUZdRq6k5qE/U09Tr1MbWX+p3GoRnQ7GheNAEthZZNm04rpVXS6mmHaWdoN2k9tA90Ol2HbkV3p4fRU+m59Nn0ZfRN9EZ6G/0qvZvex2Aw9Bh2DB9GNEPIUDBKGRsYexgnGNcYPYxPTFWmMdOZGcJMY0qZi5iVzF3M48xrzGfMfhUNFQsVL5VoFbHKTJWVKttVWlUuq/So9LM0WVYsH1YCK5e1kFXF2sc6w3rAeq+qqmqq6qkaqypRXaBapbpf9ZzqY9XPbC22LTuQPZFdxF7B3sFuY99lv+dwOJYcP04aR8FZwWngnOI84nxS46o5qAnUxGrz1WrUmtSuqb1RV1G3UPdXn6w+S71S/aD6ZfXXGioalhqBGkKNeRo1Gkc0bmv0aXI1nTSjNQs0l2nu0jyv+VyLoWWpFawl1irRqtM6pdXNpXDNuIFcEXcxdzv3DLeHR+dZ8QS8XF45by+vk9erraU9RjtJe4Z2jfYx7S4dio6ljkAnX2elzgGdWzpfRhiO8B+ROWLpiH0jro34qDtS1083U7dMt1H3pu4XPb5esF6e3mq9Zr2H+lR9W/1Y/en6m/XP6L8eyRvpPVI0smzkgZH3DEgDW4M4g9kGdQaXDPoMjQxDDWWGGwxPGb420jHyM8o1Wmt03OiFMdd4nLHEeK3xCeOXfG2+Pz+fX8U/ze81MTAJMyky2WbSadJvamWaaLrItNH0oRnLzMMsy2ytWbtZr7mx+XjzOea7ze9ZqFh4WORYrLfosPhoaWWZbLnEstnyuZWulcBqltVuqwfWHGtf62nWtdY3bOg2HjZ5NptsrtiStq62ObY1tpftSDs3O4ndJruro2ijPEdJR9WOum3Ptve3L7bfbf/YQcch0mGRQ7PDm9Hmo9NGrx7dMfq7o6tjvuN2x/tOWk7hToucWp3eOds6i5xrnG+4cFxCXOa7tLi8HWM3JnPM5jF3XLmu412XuLa7fnNzd5O77XN74W7unu6+0f22B88jxmOZxzlPmmeA53zPo56fvdy8FF4HvP70tvfO897l/Xys1djMsdvHdvuY+gh9tvl0jeOPSx+3dVyXr4mv0LfW94mfmZ/Yr97vmb+Nf67/Hv83AY4B8oDDAR8DvQLnBrYFUYJCg8qCOoO1ghODq4MfhZiGZIfsDukNdQ2dHdoWRguLCFsddltgKBAJGgS94e7hc8NPR7Aj4iOqI55E2kbKI1vHk+PDx68Z/yDKIkoa1RyNaEH0muiHMVYx02J+iaXHxsTWxD6Nc4qbE9cRz42fEr8r/kNCQMLKhPuJ1olFie1J6kkTkxqSPiYHJVckd6WMTpmbcjFVP1WS2pLGSEtKq0/rmxA8Yd2EnomuE0sn3ppkNWnGpPOT9SfnTz42RX2KcMrBdFp6cvqu9K/CaGGtsC9DkLExo1cUKFoveiX2E68Vv8j0yazIfJblk1WR9TzbJ3tN9osc35zKnNeSQEm15G1uWO6W3I950Xk78gbyk/MbC5gF6QVHpFrSPOnpqUZTZ0y9KrOTlcq6pnlNWzetVx4hry8kCicVtih4CpniUpF10Q9Fj4vHFdcUf5qeNP3gDM0Z0hmXZtrOXDrz2ayQWT/Nps4WzW6fYzJn4ZzHc/3nbptHzMuY1z7fbH7J/J4FoQt2LmQtzFv46yLHRRWL/lqcvLi1xLBkQUn3D6E/7C5VK5WX3l7ivWTLj9QfJT92LnVZumHp9zJx2YVyx/LK8q/LRMsuLHdaXrV8YEXWis6Vbis3r6Kvkq66tdp39c4KzYpZFd1rxq9pWstfW7b2r3VT1p2vHFO5ZT1rfdH6rqrIqpYN5htWbfhanVN9syagpnGjwcalGz9uEm+6ttlv874thlvKt3zZKtl6Z1votqZay9rKOnpdcd3T7UnbO37y+KmhXr++vP7bDumOrp1xO083uDc07DLYtXI3ubto94s9E/dc2Ru0t2Wf/b5tjTqN5fuxv2j/y5/Tf751IOJA+0GPg/sOWRzaeJh7uKyJaJrZ1Nuc09zVktpy9Uj4kfZW79bDvzj8suOoydGaY9rHVh5nHS85PnBi1om+Nlnb65PZJ7vbp7TfP5Vy6sbp2NOdZyLOnDsbcvZUh3/HiXM+546e9zp/5ILHheaLbhebLrleOvyr66+HO906my67X2654nml9erYq8ev+V47eT3o+tkbghsXb0bdvHor8dad2xNvd90R33l+N//u23vF9/rvL3hAe1D2UONh5SODR7W/2fzW2OXWdexx0ONLT+Kf3O8Wdb/6vfD3rz0lTzlPK58ZP2t47vz86IuQF1deTnjZ80r2qv916R+af2x8Y/3m0J9+f17qTenteSt/O/Bu2Xu99zv+GvNXe19M36MPBR/6P5Z90vu087PH544vyV+e9U//yvha9c3mW+v3iO8PBgoGBmRCuRAAQAFAZmUB73YAnFSAewVgTRjcqQEAxOB/ADC4g/xvHty7AQBuQB2AZD8gug3YCsBqAcBpA2L8gAQ/kC4uyuffUZjl4jzopdoM0CoHBt4nAwwb4NvtgYH+5oGBb/UA5R7Q9mFwlwcAqgSwrgOxcPN113tn8F/xL2AUEjlEqYDhAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF9mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDAgNzkuMTYwNDUxLCAyMDE3LzA1LzA2LTAxOjA4OjIxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTExLTA1VDE1OjUyOjMzKzAyOjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUxMTI1YTgyLWY2YmUtNDY1ZC05NDAyLTk0NWNhZGFjYTkzZSIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjJiNjlhZDExLTI0M2MtMzk0Ny05NDM3LWNiZjkzNTM3ODFiYyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSLine truncated
background-repeat: no-repeat, repeat;
background-position: right .7em top 50%, 0 0;
background-size: .65em auto, 100%;
}
.arrow:after {
border-top-color: $lens-menu-hl;
}
}
.popover-body {
color: $lens-text-color;
li.list-group-item {
background-color: inherit;
padding: 0.5rem 0.75rem;
a {
color: var(--lens-text-color-light);
}
}
}
.popover-header{
color: $lens-text-color;
background-color: $lens-main-bg;
border-bottom: 0px;
i.material-icons {
position: relative;
top: 3px;
font-size: 16px;
}
}
-72
View File
@@ -1,72 +0,0 @@
// from Lens Dashboard
$lens-main-bg: #1e2124 !default; // dark bg
$lens-pane-bg: #262b2f !default; // all panels main bg
$lens-dock-bg: #2E3136 !default; // terminal and top menu bar
$lens-menu-bg: #36393E !default; // sidemenu on left
$lens-menu-hl: #414448 !default; // sidemenu on left, top left corner
$lens-text-color: #87909c !default;
$lens-text-color-light: #a0a0a0 !default;
$lens-primary: #3d90ce !default;
// export as css variables
* {
--lens-main-bg: #{$lens-main-bg}; // dark bg
--lens-pane-bg: #{$lens-pane-bg}; // all panels main bg
--lens-dock-bg: #{$lens-dock-bg}; // terminal and top menu bar
--lens-menu-bg: #{$lens-menu-bg}; // sidemenu on left
--lens-menu-hl: #{$lens-menu-hl}; // sidemenu on left, top left corner
--lens-text-color: #{$lens-text-color};
--lens-text-color-light: #{$lens-text-color-light};
--lens-primary: #{$lens-primary};
}
// Base grayscale colors definitions
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #6c757d !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #1e2124 !default;
$black: #000 !default;
// Base colors definitions
$blue: #3d90ce !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #e83e8c !default;
$red: #CE3933 !default;
$orange: #fd7e14 !default;
$yellow: #ffc107 !default;
$green: #4caf50 !default;
$teal: #20c997 !default;
$cyan: #6ca5b7 !default;
// Theme color default definitions
$primary: $lens-primary !default;
$secondary: $gray-600 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-100 !default;
$dark: $gray-800 !default;
// This table defines the theme colors (variant names)
$theme-colors: () !default;
$theme-colors: map-merge(
(
'primary': $primary,
'secondary': $secondary,
'success': $success,
'info': $info,
'warning': $warning,
'danger': $danger,
'light': $light,
'dark': $dark
),
$theme-colors
);
-2
View File
@@ -1,2 +0,0 @@
@import "~typeface-roboto/index.css";
@import "~material-design-icons/iconfont/material-icons.css";
-182
View File
@@ -1,182 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1303.8 737.4" style="enable-background:new 0 0 1303.8 737.4;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_2_);}
.st1{fill:#393C40;}
.st2{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:9.54,9.54;}
.st3{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:10.35,10.35;}
.st4{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:9.06,9.06;}
.st5{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:12.67,12.67;}
.st6{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:12.81,12.81;}
.st7{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:10.4,10.4;}
.st8{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:9.25,9.25;}
.st9{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:9.55,9.55;}
.st10{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:9.66,9.66;}
.st11{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:10.37,10.37;}
.st12{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:10,10;}
.st13{fill:none;stroke:#393C40;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:11.37,11.37;}
.st14{clip-path:url(#SVGID_4_);}
.st15{fill:#2E3136;}
.st16{clip-path:url(#SVGID_6_);}
.st17{clip-path:url(#SVGID_8_);}
</style>
<title>Artboard 1</title>
<g>
<defs>
<rect id="SVGID_1_" width="1303.8" height="737.4"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g class="st0">
<path class="st1" d="M715.5,222.6c0.2-6.2-4-12.2-9.6-15V190c-2.1,1.9-4.9,3-7.8,3c-1.8,0-3.6-0.5-5.2-1.4v15.9
c-5.6,2.6-9.3,8.2-9.6,14.4c-0.1,2.7,0.6,5.5,1.8,7.9c-0.1-0.8-0.2-1.7-0.2-2.5c0.2-7.8,7.1-14.5,14.7-14.2s14.4,7.4,14.2,14.9
c0,0.4,0,0.8-0.1,1.2C714.8,227.2,715.4,225,715.5,222.6"/>
</g>
</g>
<line class="st2" x1="870.3" y1="376.4" x2="870.3" y2="519.5"/>
<line class="st3" x1="936.8" y1="407.1" x2="936.8" y2="562.4"/>
<line class="st4" x1="1034.7" y1="581.8" x2="1034.7" y2="627.1"/>
<line class="st5" x1="967.1" y1="547" x2="967.1" y2="585"/>
<line class="st6" x1="1104.2" y1="542.4" x2="1104.2" y2="580.8"/>
<line class="st7" x1="1036.7" y1="279.6" x2="1036.7" y2="435.6"/>
<line class="st8" x1="1106.2" y1="240.8" x2="1106.2" y2="361"/>
<line class="st9" x1="1204" y1="436.5" x2="1204" y2="522.4"/>
<line class="st10" x1="1134.5" y1="475.5" x2="1134.5" y2="562.4"/>
<line class="st11" x1="1064.9" y1="436.5" x2="1064.9" y2="467.6"/>
<line class="st12" x1="1006.4" y1="370.6" x2="1006.4" y2="460.6"/>
<line class="st13" x1="965.1" y1="240.8" x2="965.1" y2="297.6"/>
<g>
<defs>
<rect id="SVGID_3_" width="1303.8" height="737.4"/>
</defs>
<clipPath id="SVGID_4_">
<use xlink:href="#SVGID_3_" style="overflow:visible;"/>
</clipPath>
<g class="st14">
<path class="st15" d="M773.7,590.9c-8.2-4.8-8.2-12.5,0-17.3L823,545c8.2-4.8,21.7-4.8,30,0l49.4,28.6c8.2,4.8,8.2,12.5,0,17.3
L853,619.5c-8.2,4.8-21.7,4.8-30,0L773.7,590.9z"/>
<path class="st15" d="M921.8,676.7c8.2,4.8,21.7,4.8,30,0l49.4-28.6c8.2-4.8,8.2-12.5,0-17.3l-49.4-28.6c-8.2-4.8-21.7-4.8-30,0
l-49.4,28.6c-8.2,4.8-8.2,12.5,0,17.3L921.8,676.7z"/>
<path class="st15" d="M1020.7,733.8c8.2,4.8,21.7,4.8,30,0l49.3-28.6c8.2-4.8,8.2-12.5,0-17.3l-49.4-28.6c-8.2-4.8-21.7-4.8-30,0
l-49.4,28.6c-8.2,4.8-8.2,12.5,0,17.3L1020.7,733.8z"/>
<path class="st15" d="M1119.5,676.7c8.2,4.8,21.7,4.8,30,0l49.4-28.6c8.2-4.8,8.2-12.5,0-17.3l-49.4-28.6c-8.2-4.8-21.7-4.8-30,0
l-49.4,28.6c-8.2,4.8-8.2,12.5,0,17.3L1119.5,676.7z"/>
<path class="st15" d="M1248.3,545.1c-8.2-4.8-21.7-4.8-30,0l-49.4,28.6c-8.2,4.8-8.2,12.5,0,17.3l49.4,28.6c8.2,4.8,21.7,4.8,30,0
l49.4-28.5c8.2-4.8,8.2-12.5,0-17.3L1248.3,545.1z"/>
<path class="st15" d="M942.8,369.3c-3.5,2.3-5.7,6-5.9,10.2V391c0,3.7-2.7,5.2-6,3.4l-51.6-29.2c-3.5-2.2-5.7-6-6-10.1v-22.7
l1.1-0.6c0.6-0.3,3.8,0.9,7,2.7l48.6,27.3c3.7,1.8,8.1,1.8,11.9,0l49.1-28.3c3.3-1.9,6.7-3.1,7.7-2.8c1.1,0.5,1.8,1.6,1.8,2.8
c0,1.2-2.6,3.7-5.9,5.6L942.8,369.3z M942.8,292.3c-3.7-1.9-8.1-1.9-11.8,0l-58.7,33.9c-3.5,2.2-5.7,6-5.9,10.1v22.9
c0.2,4.1,2.5,7.9,6,10.1l58.7,33.2c3.7,1.9,8.1,1.9,11.9,0l58.7-33.2c3.5-2.2,5.7-6,6-10.1v-22.8c-0.2-4.1-2.4-7.9-5.9-10.1
L942.8,292.3z"/>
<path class="st15" d="M1140.4,437.4c-3.5,2.3-5.7,6-5.9,10.2v11.6c0,3.7-2.7,5.2-5.9,3.4l-51.6-29.2c-3.5-2.2-5.7-6-5.9-10.1
v-22.8l1.1-0.6c0.6-0.3,3.8,0.9,7,2.7l48.5,27.3c3.7,1.9,8.1,1.9,11.9,0l49.2-28.3c3.3-1.9,6.7-3.1,7.7-2.8
c1.1,0.5,1.8,1.6,1.8,2.8c0,1.2-2.6,3.7-5.9,5.6L1140.4,437.4z M1140.4,360.4c-3.7-1.9-8.1-1.9-11.8,0l-58.7,33.9
c-3.5,2.2-5.7,6-5.9,10.1v22.9c0.2,4.1,2.5,7.9,5.9,10.1l58.7,33.2c3.7,1.9,8.1,1.9,11.9,0l58.7-33.2c3.5-2.2,5.7-6,5.9-10.1
v-22.9c-0.2-4.1-2.4-7.9-5.9-10.1L1140.4,360.4z"/>
<path class="st15" d="M1041.6,241c-3.5,2.3-5.7,6-5.9,10.2v11.6c0,3.7-2.7,5.2-5.9,3.4L978.1,237c-3.5-2.2-5.7-6-6-10.1v-22.8
l1.1-0.6c0.6-0.3,3.8,0.9,7,2.7l48.5,27.3c3.7,1.9,8.1,1.9,11.9,0l49.2-28.3c3.3-1.9,6.7-3.1,7.7-2.8c1.1,0.5,1.8,1.6,1.8,2.8
c0,1.2-2.6,3.7-5.9,5.6L1041.6,241z M1041.6,164c-3.7-1.9-8.1-1.9-11.8,0L971,197.9c-3.5,2.2-5.7,6-5.9,10.1v22.8
c0.2,4.1,2.5,7.9,6,10.1l58.7,33.2c3.7,1.9,8.1,1.9,11.9,0l58.7-33.2c3.5-2.2,5.7-6,5.9-10.1v-22.8c-0.2-4.1-2.4-7.9-5.9-10.1
L1041.6,164z"/>
<path class="st15" d="M1041.6,538.7c-3.5,2.3-5.7,6-5.9,10.2v11.6c0,3.7-2.7,5.2-5.9,3.4l-51.6-29.2c-3.5-2.2-5.7-6-6-10.1v-22.8
l1.1-0.6c0.6-0.3,3.8,0.9,7,2.7l48.5,27.3c3.7,1.8,8.1,1.8,11.9,0l49.2-28.3c3.3-1.9,6.7-3.1,7.7-2.8c1.1,0.5,1.8,1.6,1.8,2.8
c0,1.2-2.6,3.7-5.9,5.6L1041.6,538.7z M1041.6,461.7c-3.7-1.9-8.1-1.9-11.8,0L971,495.6c-3.5,2.2-5.7,6-5.9,10.1v22.9
c0.2,4.1,2.5,7.9,6,10.1l58.7,33.2c3.7,1.9,8.1,1.9,11.9,0l58.7-33.2c3.5-2.2,5.7-6,5.9-10.1v-22.9c-0.2-4.1-2.4-7.9-5.9-10.1
L1041.6,461.7z"/>
<path class="st1" d="M582.5,50.3l-0.1-0.1L582.5,50.3 M396.4,166.1c-6.8,6.8-6.1,10.1,2.8,13.7c10.5,4.3,17.5,12,21.7,22.5
c2.9,7.2,6.9,8.1,12.4,2.6c16-15.9,26.1-26,42.2-41.8c3.9-4,8.2-9.3,8.9-16c1.4-10.3,5-17,9-21c19.1-19,27.1-27.7,40.5-41.3
c1.6-1.7,2.5-4.1,2.4-6.5c-1.6-10.5,1.7-18.2,10.5-23.3c8.2-4.7,16.9-4,24.6,2.1c0.3,0.2,1.7,1.7,1.8,1.9c0,0,0.2,0.1,4.4-4.1
l4.8-4.8c-3.1-3.1-22.3-22.2-24.7-24.4c-4.9-4.7-3.9-4.7-4.9-3.7L527,47.2c0,0-3.8,3.8-9.6-1.9C517.8,45,436.3,126.2,396.4,166.1"
/>
<path class="st1" d="M180.6,384c0,0.3,0,0.6,0,0.9C180.6,384.5,180.6,384.2,180.6,384 M362.3,256.2c-10.5-4.3-17.5-12-21.7-22.5
c-2.9-7.2-6.9-8.1-12.4-2.6c-16,15.9-31.8,32-47.9,47.8c-3.2,3.2-4.9,6.3-4,10.7c1.8,8.8-1.6,15.1-8.1,21.2
c-12,11.2-22.9,22.5-34.4,34.3c11.9,4,22.6,15.8,27,29l104.2-104.2C372,263,371.2,259.8,362.3,256.2"/>
<path class="st1" d="M616.5,60.2c-0.2,0.2-0.3,0.4-0.3,0.6C616.2,60.6,616.3,60.4,616.5,60.2 M616.2,60.9c0,0,0,0.1,0,0.1
C616.1,61,616.1,60.9,616.2,60.9 M616.5,61.6c0.1,0.1,0.1,0.1,0.2,0.2C616.6,61.7,616.5,61.7,616.5,61.6 M626.9,61.2
c-0.1-3.3-2.6-6-5.9-6.2c-3.3,0-6,2.6-6.1,5.9c-0.1,3.3,2.5,6,5.8,6.2C624,67,626.7,64.5,626.9,61.2"/>
<path class="st1" d="M575.9,72.3c-0.1-10.5-8.7-18.9-19.2-18.8s-18.9,8.7-18.8,19.2c0,10.5,9,19.4,19.2,19.2
C567.8,91.7,576.1,82.9,575.9,72.3"/>
<path class="st1" d="M188.9,482h-53c-12,0-12-12-12-12v-71c0,82,65,82.8,65,82.8C200.9,482,188.9,482,188.9,482 M223.2,483v-46
c0-6-6-6.3-6-6.3c-11.7-0.9-22.6-6.1-30.7-14.5c-10-10.7-11.2-24.1-10.8-32.4c0.3-6-5.7-6-5.7-6h-47c-6,0-6,6-6,6V483c0,6,6,6,6,6
h94.2C223.2,489,223.2,483,223.2,483"/>
<path class="st1" d="M2.2,363.7c0,0-0.6-1.2-0.8-1.1l-1,0.4c0,0-0.3,0.1-0.3-5.9v-24.4c0,0,0-6,2.4-6s11.8-0.4,16.2-0.4
c11,0,13.1,2.1,13.1,13.2c0,12.6,0,25.2,0,32.2c0,0,0,6,3.8,5.7s4.8,3.7,4.8,9.5c-0.2,27.7-0.2,55.4,0,83.1c0,6.2,0.2,12-4.2,11.8
c0,0-4.4-0.1-4.4,5.9c0,7.9,0.1,21.2,0,34.5c0,6.5-2.9,10.1-8.9,10.4s-16.8,0.4-19.9,0.4c0,0-3.1,0-3.1-6v-24.6c0,0,0-6,0.3-5.9
l1.2,0.3c0,0,0.3,0.1,0.8-1.1s2.5-3.7,2.5-6.1c0.2-40,0.2-79.9,0-119.9C5,367.5,2.9,365,2.2,363.7"/>
<path class="st1" d="M114.9,469.9l-1.5,1.2c0,0-0.3,0.2-0.8-0.5s-2.3-2.1-2.3-3.5c-0.2-23.2-0.2-46.4,0-69.6c0-1.3,2-2.6,2.5-3.2
c0,0,0.5-0.6,0.8-0.5l1.3,0.9 M44.9,432.6c0-15.2,0.1-30.5,0-45.8c0-5.4,2.1-7.9,7.6-7.8c10.5,0.2,37.1,0.1,47.6,0
c5.4-0.1,7.9,2.2,7.9,7.6c-0.1,30.5-0.1,61,0,91.5c0,5.3-2.2,8-7.7,7.9c-10.3-0.1-36.7-0.1-47,0c-5.8,0.1-8.6-2.3-8.5-8.4
C45,462.6,44.8,447.6,44.9,432.6"/>
<path class="st1" d="M218.4,427.1c-24.7-0.2-40.3-21.7-39.5-41.9c1-24.1,19.7-40.8,43.9-39.6c21.8,1.1,38.3,20.9,37,44.4
C258.6,410.9,239.4,428.2,218.4,427.1"/>
<path class="st15" d="M692.1,363.5c4.6,2.3,10,2.2,14.5-0.1l60.3-34.7c4-2.3,8.2-3.8,9.4-3.4c1.1,0.5,1.9,1.5,2.2,2.7
c0,0,0,0.7,0,1.4s-3.2,3.8-7.2,6.1l-63.5,37.1c-4,2.3-7.2,7.9-7.2,12.5v14.2c0,4.5-3.3,6.4-7.3,4.1L630,367.7
c-4-2.3-7.3-7.8-7.3-12.4v-27.9c0,0,0,0,0.3-0.2l1-0.6c0.7-0.4,4.6,1.1,8.6,3.3L692.1,363.5z M693.3,278.3l-72,41.5
c-4.1,2.7-6.7,7.1-7.2,11.9l1.7,0.2c1.2,0.2,2,1.3,1.9,2.5v8c0,0,0,1.1-3.7,2.8v14.9c0,4.5,3.3,10.1,7.3,12.4l71.9,40.7
c4.6,2.3,10,2.3,14.6,0l71.9-40.7c4-2.3,7.3-7.8,7.3-12.4v-12c-0.3-0.1-0.6-0.1-0.8-0.2c-8.1-2.7-8.1-4.6-8.1-4.6v-8
c-0.1-1.2,0.7-2.3,1.9-2.5l7-1c-0.4-4.9-3.1-9.4-7.2-12.1l-72-41.5C703.2,276,697.9,276,693.3,278.3"/>
<path class="st1" d="M487.8,147L487.8,147l35.2-36.8l-5.1-4.9l-23,23.7C494.9,129,488.9,135,487.8,147"/>
<path class="st1" d="M519.9,43.1L519.9,43.1L563.6,0l5.3,5.2l-14,14.4l-1.3-1.3l-27.2,26.1C526.4,44.5,523.8,47.1,519.9,43.1"/>
<path class="st1" d="M631.5,61.3l-7.1,7.1c0,0-4.2,4.2,0,8.5l62.5,62.6c0,0,4.2,4.2,8.5,0l2.8-2.8c0,0,4.2-4.2,0-8.5L631.5,61.3z"
/>
<path class="st1" d="M709.3,166.1L594.9,51.7c0,0-4.2-4.2-8.5,0l-2.8,2.8c0,0-4.2,4.2,0,8.5l24.1,24.1l-11.6,11.6
c0,0-4.2,4.2,0,8.5l86,86c0,0,4.2,4.2,8.5,0l1.9-1.9c-3.5-2.2-5.6-6-5.5-10.1c0.2-6.2,5.7-11.5,11.6-11.2c3.9,0.3,7.4,2.4,9.5,5.7
l1.2-1.2C709.3,174.5,713.5,170.3,709.3,166.1 M690.2,182.8c-0.1-0.1-0.2-0.2-0.3-0.3L690.2,182.8 M690.4,183l-0.1-0.1L690.4,183
M689.4,181c0.1-0.4,0.3-0.8,0.7-1.1C689.7,180.2,689.4,180.6,689.4,181"/>
<path class="st1" d="M708.4,138.2c0,0-4.2-4.2-8.5,0L697,141c0,0-4.2,4.2,0,8.5l14,13.9L708.4,138.2z"/>
<path class="st1" d="M693,228.7c0.1-0.3,0.2-0.6,0.5-0.8C693.2,228.1,693.1,228.4,693,228.7 M693.7,230c-0.1-0.1-0.2-0.2-0.3-0.3
C693.5,229.8,693.6,229.9,693.7,230 M707.3,229.2c-0.3,4.3-3.9,7.7-8.2,7.8c-4.4-0.2-7.8-3.8-7.7-8.2c0.2-4.4,3.7-7.8,8.1-7.8
C703.8,221.3,707.2,224.9,707.3,229.2 M690.5,227.4c-0.3,0.3-0.6,0.7-0.7,1.1C689.9,228,690.2,227.6,690.5,227.4 M724.9,239v-3
c0,0,0-5-5-5h-8.8c0.1-0.5,0.2-1.1,0.2-1.6c0.2-6.2-5.4-12.1-11.7-12.4s-12,5.3-12.2,11.7c0,0.8,0,1.5,0.2,2.3h-8.7c0,0-5,0-5,5v3
c0,0,0,5,5,5h41C719.9,244,724.9,244,724.9,239"/>
<path class="st1" d="M707.7,181.8c-0.1-5-4-9.1-9-9.5c-4.8-0.2-9.2,4.1-9.4,9c-0.1,5.1,3.8,9.3,8.9,9.4
C703.2,190.6,707.4,186.7,707.7,181.8"/>
</g>
</g>
<polygon class="st1" points="570.8,7.1 570.8,7.1 568.9,9 615.4,55.5 617.3,53.6 "/>
<g>
<defs>
<rect id="SVGID_5_" width="1303.8" height="737.4"/>
</defs>
<clipPath id="SVGID_6_">
<use xlink:href="#SVGID_5_" style="overflow:visible;"/>
</clipPath>
<g class="st16">
<path class="st1" d="M586.1,44.3l-25.1-25.1c-0.4-0.4-1-0.4-1.4,0c0,0-0.1,0.1-0.1,0.1l-1.8,1.8c-0.1,0.1-0.1,0.2-0.2,0.3
L583.9,48l0.1-0.1l0,0c0.1-0.1,0.2-0.1,0.3-0.2l1.7-1.9C586,45.8,586.8,45,586.1,44.3"/>
</g>
</g>
<polygon class="st1" points="540.7,88.9 540.7,88.9 538.8,87 521,105.1 522.9,107 "/>
<g>
<defs>
<rect id="SVGID_7_" width="1303.8" height="737.4"/>
</defs>
<clipPath id="SVGID_8_">
<use xlink:href="#SVGID_7_" style="overflow:visible;"/>
</clipPath>
<g class="st17">
<path class="st1" d="M1123.8,193.5c0.3,0.3,0.1,0.5-0.2,0.7c-2.5,1.6-5,3.1-7.4,4.8c-2,1.3-3.6,3-4.7,5.1
c-0.8,1.6-0.7,3.6,0.3,5.2c1.3,2,3,3.7,5.1,5c2.2,1.5,4.5,2.9,6.7,4.3c0.3,0.2,0.5,0.3,0.2,0.6c-1.7-0.9-3.4-1.9-5.1-2.7
c-2.1-1.1-4.3-2-6.6-2.6c-1.8-0.6-3.7-0.5-5.4,0.2c-1.3,0.7-2.4,2-2.8,3.4c-0.7,1.8-1.1,3.6-1.2,5.5c-0.3,2.8-0.4,5.7-0.3,8.5
c0,0.1,0,0.2,0,0.2h-0.6c0-0.2,0-0.3,0-0.5c-0.1-2.3-0.1-4.6-0.3-6.9c-0.1-2.5-0.6-4.9-1.5-7.2c-0.6-1.9-2.2-3.3-4.2-3.6
c-1.4-0.2-2.8-0.1-4.1,0.4c-1.8,0.6-3.5,1.2-5.2,2c-2.1,1-4.2,2.1-6.3,3.2l-0.2-0.3c0.1-0.1,0.3-0.2,0.4-0.3
c2.5-1.6,5-3.1,7.4-4.9c1.9-1.3,3.5-3,4.6-5c0.9-1.7,0.7-3.7-0.3-5.2c-1.3-2-3-3.7-5-4.9c-2.2-1.5-4.5-2.9-6.7-4.3
c-0.3-0.2-0.5-0.3-0.2-0.6c1.8,1,3.6,2,5.4,2.9c2.2,1.1,4.4,2,6.8,2.6c1.3,0.3,2.6,0.4,3.9,0.1c1.7-0.5,3-1.7,3.6-3.3
c0.8-1.9,1.3-4,1.4-6.1c0.2-2.3,0.2-4.6,0.4-6.9c0-0.5,0.1-1,0.1-1.5c0-0.1,0.2-0.2,0.3-0.3l0.2,0.1c0,0.4,0,0.9,0,1.3
c0,1.5,0,3,0.1,4.5c0.1,2.9,0.6,5.7,1.5,8.4c0.8,2,2,3.5,4.2,3.9c1.6,0.2,3.2,0.1,4.7-0.4c3.5-1.2,6.8-2.7,10-4.6L1123.8,193.5"/>
<path class="st1" d="M654.9,259c-9,0-14-0.2-22,0c-5,0.1-17,0-17.8,1.1c-4.3,6.1-7.5,14.1-8,21.6c-1.1,17-0.4,34.2-0.4,51.8l8,1.1
v8.6c-15.6,5.2-21.9-3.5-21.7-17.2c0.2-15.7-0.2-31.4,0.2-47.1c0.1-6.4,1.3-12.7,2.1-19.5c-7.4-0.6-5.1-6.6-4.5-12.4h89V259"/>
<path class="st1" d="M731.9,259c9,0,27-0.2,35,0c5,0.1,12.2-0.6,14,2c4.3,6.2,7.5,13.4,8,20.8c1.1,17,0.4,34.2,0.4,51.8l-8,1.1
v8.6c15.6,5.2,21.9-3.5,21.7-17.2c-0.2-15.7,0.2-31.4-0.2-47.1c-0.1-6.4-1.3-12.7-2.1-19.5c7.4-0.6,5.1-6.6,4.5-12.4h-89v12"/>
<path class="st1" d="M275.1,286c-7.5,7.5-44.8,44.5-58.3,57.9c2.7-0.3,5.3-0.4,8-0.3c2.1,0.1,4.1,0.4,6.1,0.8
c3.1-3.2,7-7.3,12.1-12.5c8.1-8.3,15.8-16.3,24.2-24.2c4.6-4.3,7-8.7,5.8-15C272.5,290.3,273.2,287.7,275.1,286 M275.8,285.2
c-0.2,0.2-0.5,0.5-0.7,0.8C276.5,284.6,276.9,284.2,275.8,285.2"/>
<path class="st1" d="M390.6,217c0,5.9-4.8,10.6-10.7,10.6c-5.9,0-10.6-4.8-10.6-10.7c0-5.8,4.7-10.5,10.4-10.6
C385.7,206.3,390.5,211.1,390.6,217 M419,217c0-21.6-17.5-39.1-39.1-39.1s-39.1,17.5-39.1,39.1c0,21.6,17.5,39.1,39.1,39.1
S419,238.6,419,217L419,217"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

-10
View File
@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 497.43 228.97">
<defs><style>.cls-1{fill:#3f90ce;}</style></defs>
<title>Lens logo</title>
<g>
<path class="cls-1" d="M0,0H12.25V170H110v11.64H0Z"/>
<path class="cls-1" d="M138.75,167.24Q122.82,148,122.82,116.39t15.93-50.84q15.93-19,42.27-19T223.29,64q15.92,17.46,15.93,46.55v5.82H135.08q0,26.65,12.55,42.58T181,174.9a50.14,50.14,0,0,0,28.18-8.27A44.23,44.23,0,0,0,227,144.27h12.25q-3.08,18.07-19.61,30T181,186.23Q154.68,186.23,138.75,167.24ZM227,104.75v-4.59a46.49,46.49,0,0,0-14.71-30.63A45.05,45.05,0,0,0,181,57.28a43.4,43.4,0,0,0-30.94,12.56,47.58,47.58,0,0,0-15,31.54v3.37Z"/>
<path class="cls-1" d="M268,181.64V51.15h12.25V73.21a41.83,41.83,0,0,1,16.85-18.38,46.6,46.6,0,0,1,25.12-7q19.29,0,32.77,12.56Q368.79,73.21,368.78,91v90.67H356.53V91a30.37,30.37,0,0,0-10.11-23,34.12,34.12,0,0,0-24.19-9.5A40.35,40.35,0,0,0,295,68.92a45.21,45.21,0,0,0-14.7,26.65v86.07Z"/>
<path class="cls-1" d="M409.83,172.75Q395.12,160.51,395.12,143h11.34q0,13.17,11.33,22.67,11.32,9.18,27.26,9.19,18.38,0,29.71-8.27a27,27,0,0,0,11-22.36q0-10.41-11.64-17.77-11.64-7-31.24-8.88-20.84-1.84-33.08-11.64-12.57-9.8-12.56-24.2,0-16.23,12.56-25.11t35.22-8.89q21.75,0,34.61,9.5,13.17,9.5,13.17,25.42H481.19A28.06,28.06,0,0,0,469.55,64q-9.49-6.73-24.5-6.74-16.55,0-26.34,6.74t-9.8,17.76q0,10.43,11.94,17.77,11.64,7,31.25,8.88,20.5,1.84,33.08,12.25,12.24,10.42,12.25,26,0,17.46-14.4,27.87Q468.95,185,445.05,185,424.22,185,409.83,172.75Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

-274
View File
@@ -1,274 +0,0 @@
<template>
<div class="content">
<b-container fluid class="h-100">
<b-row align-h="around">
<b-col lg="7">
<div class="card">
<h2>Add Cluster</h2>
<div class="add-cluster">
<b-form @submit.prevent="doAddCluster">
<b-form-group
label="Choose config:"
>
<b-form-select
v-model="kubecontext"
:options="contextNames"
@change="onSelect($event)"
/>
<b-button v-b-toggle.collapse-advanced variant="link">
Proxy settings
</b-button>
</b-form-group>
<b-collapse id="collapse-advanced">
<b-form-group
label="HTTP Proxy server. Used for communicating with Kubernetes API."
description="A HTTP proxy server URL (format: http://<address>:<port>)."
>
<b-form-input
v-model="httpsProxy"
/>
</b-form-group>
</b-collapse>
<b-form-group
label="Kubeconfig:"
v-if="status === 'ERROR' || kubecontext === 'custom'"
>
<div class="editor">
<prism-editor v-model="clusterconfig" language="yaml" />
</div>
</b-form-group>
<b-alert variant="danger" show v-if="status === 'ERROR'">
{{ errorMsg }}
<div v-if="errorDetails !== ''">
<b-button v-b-toggle.collapse-error variant="link" size="sm">
Show details
</b-button>
<b-collapse id="collapse-error">
<code>
{{ errorDetails }}
</code>
</b-collapse>
</div>
</b-alert>
<b-form-row>
<b-col>
<b-button variant="primary" type="submit" :disabled="clusterconfig === ''">
<b-spinner small v-if="isProcessing" label="Small Spinner" />
{{ addButtonText }}
</b-button>
</b-col>
</b-form-row>
</b-form>
</div>
</div>
</b-col>
<b-col lg="5" class="help d-none d-lg-block">
<h3>Clusters associated with Lens</h3>
<p>
Add clusters by clicking the <span class="text-primary">Add Cluster</span> button.
You'll need to obtain a working kubeconfig for the cluster you want to add.
</p>
<p>
Each <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context">cluster context</a> is added as a separate item in the left-side cluster menu to allow you to operate easily on multiple clusters and/or contexts.
</p>
<p>
For more information on kubeconfig see <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/">Kubernetes docs</a>
</p>
<p>
NOTE: Any manually added cluster is not merged into your kubeconfig file.
</p>
<p>
To see your currently enabled config with <code>kubectl</code>, use <code>kubectl config view --minify --raw</code> command in your terminal.
</p>
<p>
When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens app.
</p>
<a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#option-1-oidc-authenticator">
<h4>OIDC (OpenID Connect)</h4>
</a>
<div>
<p>
When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
</p>
<b>Dedicated refresh token</b>
<p>
As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
If you share the refresh token with e.g. <code>kubectl</code> who ever uses the token first will invalidate it for the next user.
One way to achieve this is with <a href="https://github.com/int128/kubelogin">kubelogin</a> tool by removing the tokens (both <code>id_token</code> and <code>refresh_token</code>) from the config and issuing <code>kubelogin</code> command. That'll take you through the login process and will result you having "dedicated" refresh token.
</p>
</div>
<h4>Exec auth plugins</h4>
<p>
When using <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration">exec auth</a> plugins make sure the paths that are used to call any binaries are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config, Lens app might not have all login shell env variables set automatically.
</p>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import * as PrismEditor from 'vue-prism-editor'
import * as k8s from "@kubernetes/client-node"
import { dumpConfigYaml } from "../../main/k8s"
import ClustersMixin from "@/mixins/ClustersMixin";
class ClusterAccessError extends Error {}
export default {
name: 'AddClusterPage',
mixins: [ClustersMixin],
props: { },
components: {
PrismEditor,
},
data(){
return {
clusterconfig: "",
httpsProxy: "",
kubecontext: "",
status: "",
errorMsg: "",
errorCluster: "",
errorDetails: "",
seenContexts: []
}
},
mounted: function() {
this.$store.dispatch("reloadAvailableKubeContexts");
this.seenContexts = JSON.parse(JSON.stringify(this.$store.getters.seenContexts)) // clone seenContexts from store
this.storeSeenContexts()
},
computed: {
isProcessing: function() {
return this.status === "PROCESSING";
},
addButtonText: function() {
if (this.kubecontext === "custom") {
return "Add Cluster(s)"
} else {
return "Add Cluster"
}
},
contextNames: function() {
const configs = this.availableContexts
const names = configs.map((kc) => {
return { text: kc.currentContext + (this.isNewContext(kc.currentContext) ? " (new)": ""), value: dumpConfigYaml(kc) }
})
names.unshift({text: "Select kubeconfig", value: ""})
names.push({text: "Custom ...", value: "custom"})
return names;
},
},
methods: {
isNewContext(context) {
return this.newContexts.indexOf(context) > -1
},
storeSeenContexts() {
const configs = this.$store.getters.availableKubeContexts
const contexts = configs.map((kc) => {
return kc.currentContext
})
this.$store.dispatch("addSeenContexts", contexts)
},
onSelect: function() {
this.status = "";
if (this.kubecontext === "custom") {
this.clusterconfig = "";
} else {
this.clusterconfig = this.kubecontext;
}
},
isOidcAuth: function(authProvider) {
if (!authProvider) { return false }
if (authProvider.name === "oidc") { return true }
return false;
},
doAddCluster: async function() {
// Clear previous error details
this.errorMsg = ""
this.errorCluster = ""
this.errorDetails = ""
this.status = "PROCESSING"
try {
const kc = new k8s.KubeConfig();
kc.loadFromString(this.clusterconfig); // throws TypeError if we cannot parse kubeconfig
const clusterInfo = {
kubeConfig: dumpConfigYaml(kc),
preferences: {
clusterName: kc.currentContext
},
workspace: this.$store.getters.currentWorkspace.id
}
if (this.httpsProxy) {
clusterInfo.preferences.httpsProxy = this.httpsProxy
}
let res = await this.$store.dispatch('addCluster', clusterInfo)
console.log("addCluster result:", res)
if(!res){
this.status = "ERROR";
return false;
}
this.status = "SUCCESS"
this.$router.push({
name: "cluster-page",
params: {
id: res.id
},
}).catch((err) => {})
} catch (error) {
console.log("addCluster raised:", error)
if(typeof error === 'string') {
this.errorMsg = error;
} else if(error instanceof TypeError) {
this.errorMsg = "cannot parse kubeconfig";
} else if(error.response && error.response.statusCode === 401) {
this.errorMsg = "invalid kubeconfig (access denied)"
} else if(error.message) {
this.errorMsg = error.message
} else if(error instanceof ClusterAccessError) {
this.errorMsg = `Invalid kubeconfig context ${error.context}`
this.errorCluster = error.cluster
this.errorDetails = error.details
}
this.status = "ERROR";
return false;
}
return true;
}
}
}
</script>
<style scoped lang="scss">
.help{
border-left: 1px solid #353a3e;
padding-top: 20px;
&:first-child{
padding-top: 0;
}
h3{
padding: 0.75rem 0 0.75rem 0;
}
height: 100vh;
overflow-y: auto;
}
h2{
padding: 0.75rem;
}
.card {
margin-top: 20px;
}
.add-cluster {
padding: 0.75rem;
}
.btn-link {
padding-left: 0;
}
</style>
@@ -1,115 +0,0 @@
<template>
<div class="content">
<ClosePageButton />
<div class="container-fluid lens-workspaces">
<div class="header sticky-top">
<h2><i class="material-icons">layers</i> Workspaces</h2>
</div>
<div class="row">
<div class="col-3" />
<div class="col-6">
<h2>New Workspace</h2>
<b-form @submit.prevent="createWorkspace">
<b-form-group
label="Name:"
label-for="input-name"
>
<b-form-input
id="input-name"
v-model="workspace.name"
trim
/>
</b-form-group>
<b-form-group
label="Description:"
label-for="input-description"
>
<b-form-input
id="input-description"
v-model="workspace.description"
trim
/>
</b-form-group>
<b-form-row>
<b-col>
<b-button variant="primary" type="submit">
Create Workspace
</b-button>
</b-col>
</b-form-row>
</b-form>
</div>
<div class="col-3" />
</div>
</div>
</div>
</template>
<script>
import { v4 as uuid } from "uuid";
import ClosePageButton from "@/components/common/ClosePageButton";
export default {
name: 'AddWorkspacePage',
components: {
ClosePageButton
},
data() {
return {
workspace: {
id: uuid(),
name: "",
description: ""
},
errors: {
name: null,
description: null
}
}
},
computed: {
},
methods: {
createWorkspace: function() {
this.$store.commit("addWorkspace", this.workspace)
this.$router.push({
name: "workspaces-page"
})
}
},
mounted: function() {
this.$store.commit("hideMenu");
},
destroyed: function() {
this.$store.commit("showMenu");
}
}
</script>
<style lang="scss" scoped>
#app > .main-view > .content {
left: 70px;
right: 70px;
}
h2 {
i {
position: relative;
top: 6px;
}
}
.lens-workspaces {
height: 100%;
overflow-y: scroll;
& input {
background-color: #252729 !important;
border: 0px !important;
color: #87909c !important;
}
.header {
padding-top: 15px;
}
}
</style>
@@ -1,88 +0,0 @@
<template>
<div class="bottom-bar">
<div id="workspace-area">
<i class="material-icons">layers</i> {{ currentWorkspace.name }}
</div>
<b-popover target="workspace-area" triggers="click" placement="top" :show.sync="show">
<template v-slot:title>
<a href="#" @click.prevent="goWorkspaces"><i class="material-icons">layers</i> Workspaces</a>
</template>
<ul
v-for="workspace in workspaces"
:key="workspace.id"
:workspace="workspace"
class="list-group list-group-flush"
>
<li class="list-group-item">
<a href="#" @click.prevent="switchWorkspace(workspace)">{{ workspace.name }}</a>
</li>
</ul>
</b-popover>
</div>
</template>
<script>
export default {
name: "BottomBar",
data() {
return {
show: false
}
},
computed: {
currentWorkspace: function() {
return this.$store.getters.currentWorkspace;
},
workspaces: function() {
return this.$store.getters.workspaces;
}
},
methods: {
switchWorkspace: function(workspace) {
this.show = false;
this.$store.commit("setCurrentWorkspace", workspace);
this.$store.dispatch("clearClusters");
this.$store.dispatch("refreshClusters", workspace);
this.$router.push({
name: "landing-page"
})
},
goWorkspaces: function() {
this.$router.push({
name: "workspaces-page"
})
}
}
}
</script>
<style scoped lang="scss">
.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20px;
background-color: var(--lens-primary);
z-index: 2000;
}
#workspace-area {
position: absolute;
bottom: 2px;
right: 10px;
display: block;
color: #fff;
opacity: 0.9;
font-size: 11px;
cursor: pointer;
&.active{
opacity: 1.0;
}
i {
position: relative;
top: 4px;
font-size: 14px;
}
}
</style>
-158
View File
@@ -1,158 +0,0 @@
<template>
<div class="content">
<div class="h-100">
<div class="wrapper" v-if="status === 'LOADING'">
<cube-spinner text="" />
<div class="auth-output">
<!-- eslint-disable-next-line vue/no-v-html -->
<pre class="auth-output" v-html="authOutput" />
</div>
</div>
<div class="wrapper" v-if="status === 'ERROR'">
<div class="error">
<i class="material-icons">{{ error_icon }}</i>
<div class="text-center">
<h2>{{ cluster.preferences.clusterName }}</h2>
<!-- eslint-disable-next-line vue/no-v-html -->
<pre v-html="authOutput" />
<b-button variant="link" @click="tryAgain">
Reconnect
</b-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import CubeSpinner from "@/components/CubeSpinner";
import { webviewTag } from 'electron';
export default {
name: "ClusterPage",
components: {
CubeSpinner
},
data(){
return {
authOutput: ""
}
},
computed: {
cluster: function() {
return this.$store.getters.clusterById(this.$route.params.id);
},
online: function() {
if (!this.cluster) { return false }
return this.cluster.online;
},
accessible: function() {
if (!this.cluster) { return false }
return this.cluster.accessible;
},
lens: function() {
return this.$store.getters.lensById(this.cluster.id);
},
status: function() {
if (this.cluster) {
if (this.cluster.accessible && this.lens.loaded === true) {
return "SUCCESS";
} else if (this.cluster.accessible === false) {
return "ERROR";
}
return "LOADING";
}
return "ERROR";
},
error_icon: function() {
if (!this.cluster.online) {
return "cloud_off"
} else {
return "https"
}
}
},
methods: {
tryAgain: function() {
this.authOutput = ""
this.cluster.accessible = null
setTimeout(() => {
this.loadLens()
}, 1000)
},
loadLens: function() {
this.authOutput = "Connecting ...\n";
this.$promiseIpc.on(`kube-auth:${this.cluster.id}`, (output) => {
this.authOutput += output.data;
})
this.toggleLens();
return this.$store.dispatch("refineCluster", this.$route.params.id);
},
lensLoaded: function() {
console.log("lens loaded")
this.lens.loaded = true;
this.$store.commit("updateLens", this.lens);
},
// Called only when online state changes
toggleLens: function() {
if (!this.lens) { return }
if (this.accessible) {
setTimeout(this.activateLens, 0); // see: https://github.com/electron/electron/issues/10016
} else {
this.hideLens();
}
},
activateLens: async function() {
console.log("activate lens")
if (!this.lens.webview) {
console.log("create webview")
const webview = document.createElement('webview');
webview.addEventListener('did-finish-load', this.lensLoaded);
webview.src = this.cluster.url;
this.lens.webview = webview;
}
this.$store.dispatch("attachWebview", this.lens);
this.$tracker.event("cluster", "open");
},
hideLens: function() {
this.$store.dispatch("hideWebviews");
}
},
created() {
this.loadLens();
},
destroyed() {
this.hideLens();
},
watch: {
"$route": "loadLens",
"online": "toggleLens",
"cluster": "toggleLens",
"accessible": function(newStatus, oldStatus) {
console.log("accessible watch, vals:", newStatus, oldStatus);
if(newStatus === false) { // accessble == false
this.$tracker.event("cluster", "open-failed");
}
},
}
};
</script>
<style scoped lang="scss">
div.auth-output {
padding-top: 250px;
width: 70%;
pre {
height: 100px;
text-align: center;
}
}
.error {
width: 90%;
}
pre {
font-size: 80%;
overflow: auto;
max-height: 150px;
}
</style>
@@ -1,141 +0,0 @@
<template>
<div class="settings-section">
<b>Metrics</b>
<p class="description">
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">here</a>.
</p>
<div class="actions">
<b-button @click="install" v-if="!settings.installed" :disabled="!cluster.isAdmin || isProcessing || !canInstall" variant="primary">
<b-spinner small v-if="isProcessing" label="Small Spinner" />
Install
</b-button>
<b-button @click="upgrade" v-if="isUpgradeAvailable" :disabled="!cluster.isAdmin || isProcessing || isUpgrading" variant="primary">
<b-spinner small v-if="isUpgrading" label="Small Spinner" />
Upgrade
</b-button>
<b-button @click="uninstall" v-if="settings.installed" :disabled="!cluster.isAdmin || isProcessing || isUpgrading" variant="danger">
<b-spinner small v-if="isProcessing" label="Small Spinner" />
Uninstall
</b-button>
<b-alert show variant="danger" v-if="status === 'ERROR'">
{{ errorMsg }}
</b-alert>
</div>
</div>
</template>
<script>
const semver = require('semver');
export default {
name: 'Metrics',
components: {},
props: {
cluster: {
type: Object,
required: true,
default: null
},
feature: {
type: String,
required: true,
default: null
},
settings: {
type: Object,
required: false,
default: Object
}
},
data(){
return {
status: "",
errorMsg: "",
}
},
computed:{
isUpgradeAvailable: function() {
return this.cluster.features.metrics.canUpgrade;
},
isProcessing: function() {
return this.status === "PROCESSING";
},
isUpgrading: function() {
return this.status === "UPGRADING";
},
canInstall: function() {
return !this.cluster.preferences.prometheus
}
},
methods: {
install: async function(){
this.status="PROCESSING";
let error = null;
let result = await this.$store.dispatch("installClusterFeature", {
name: this.feature,
clusterId: this.cluster.id,
config: null,
}).catch(e => {
error = e;
return false; // just returning false to promise
})
// handle exceptions here; but oh why, why do we have to???
if(error){
this.status = "ERROR";
this.errorMsg = error;
return false;
}
// now let's look at result...
if(result.success === false) {
this.status = "ERROR"
this.errorMsg = result.message
} else {
this.$store.dispatch("refineCluster", this.cluster.id);
this.status = "";
this.errorMsg = "";
}
},
uninstall: async function(){
this.status = "PROCESSING";
let error = null;
try {
let result = await this.$store.dispatch("uninstallClusterFeature", {
name: this.feature,
clusterId: this.cluster.id,
})
console.log("uninstall result:", result);
this.$store.dispatch("refineCluster", this.cluster.id);
this.status = "SUCCESS";
this.errorMsg = "";
} catch(error) {
this.status = "ERROR"
this.errorMsg = error
console.log("Error uninstalling:", error);
}
return true;
},
upgrade: async function(){
this.status = "UPGRADING";
try {
let result = await this.$store.dispatch("upgradeClusterFeature", {
name: this.feature,
clusterId: this.cluster.id,
config: null,
})
this.$store.dispatch("refineCluster", this.cluster.id);
this.status = "";
this.errorMsg = "";
} catch(error) {
this.status = "ERROR"
this.errorMsg = error.message
}
return true;
},
}
}
</script>
<style scoped lang="scss">
</style>
@@ -1,128 +0,0 @@
<template>
<div class="settings-section">
<b>User Mode</b>
<p class="description">
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>
<div class="actions">
<b-button @click="install" v-if="!settings.installed" :disabled="!cluster.isAdmin || isProcessing" variant="primary">
<b-spinner small v-if="isProcessing" label="Small Spinner" />
Install
</b-button>
<b-button @click="uninstall" v-if="settings.installed" :disabled="!cluster.isAdmin || isProcessing" variant="danger">
<b-spinner small v-if="isProcessing" label="Small Spinner" />
Uninstall
</b-button>
<b-button @click="upgrade" v-if="isUpgradeAvailable" :disabled="!cluster.isAdmin" variant="primary">
Upgrade
</b-button>
<b-alert show variant="danger" v-if="status === 'ERROR'">
{{ errorMsg }}
</b-alert>
</div>
</div>
</template>
<script>
const semver = require('semver');
export default {
name: 'UserMode',
components: {},
props: {
cluster: {
type: Object,
required: true,
default: null
},
feature: {
type: String,
required: true,
default: null
},
settings: {
type: Object,
required: false,
default: Object
}
},
data(){
return {
status: "",
errorMsg: "",
}
},
computed:{
isProcessing: function() {
return this.status === "PROCESSING";
},
isUpgradeAvailable: function() {
if(!this.settings.installed) return false;
if(!this.settings.currentVersion) return false;
if(!this.settings.latestVersion) return false;
let currentVersion = (this.settings.currentVersion.charAt(0) === "v") ? this.settings.currentVersion.substr(1) : this.settings.currentVersion;
let latestVersion = (this.settings.latestVersion.charAt(0) === "v") ? this.settings.latestVersion.substr(1) : this.settings.latestVersion;
return semver.gt(latestVersion, currentVersion)
},
},
methods: {
install: async function(){
this.status="PROCESSING";
let error = null;
let result = await this.$store.dispatch("installClusterFeature", {
name: this.feature,
clusterId: this.cluster.id,
config: null,
}).catch(e => {
error = e;
return false; // just returning false to promise
})
// handle exceptions here; but oh why, why do we have to???
if(error){
this.status = "ERROR";
this.errorMsg = error;
return false;
}
// now let's look at result...
if(result.success === false) {
this.status = "ERROR"
this.errorMsg = result.message
} else {
this.$store.dispatch("refineCluster", this.cluster.id);
this.status = "";
this.errorMsg = "";
}
},
uninstall: async function(){
this.status="PROCESSING";
let error = null;
try {
let result = await this.$store.dispatch("uninstallClusterFeature", {
name: this.feature,
clusterId: this.cluster.id,
})
console.log("uninstall result:", result);
this.$store.dispatch("refineCluster", this.cluster.id);
this.status="SUCCESS";
this.errorMsg = "";
} catch(error) {
this.status = "ERROR"
this.errorMsg = error
console.log("Error uninstalling:", error);
}
return true;
},
upgrade: async function(){
// todo
return true;
},
}
}
</script>
<style scoped lang="scss">
</style>
@@ -1,7 +0,0 @@
import Metrics from './Metrics'
import UserMode from './UserMode'
export default {
Metrics,
UserMode
}
@@ -1,51 +0,0 @@
<template>
<div id="cluster-features" class="row">
<div class="col-12">
<h2>Features</h2>
<component
v-for="featureName in features"
:key="featureName"
:is="featureName"
:cluster="cluster"
:feature="featureName"
:settings="feature(featureName)"
/>
</div>
</div>
</template>
<script>
// register all sub components
import components from './Components'
export default {
name: 'ClusterFeatures',
components: components,
props: {
cluster: {
type: Object,
default: null,
}
},
data(){
return {
}
},
computed:{
features: function() {
if (!this.cluster) return []
if (!this.cluster.features) return []
return Object.keys(this.cluster.features)
}
},
methods: {
feature: function( name ){ return this.cluster.features[name] }
}
}
</script>
<style scoped lang="scss">
h2{
margin-top: 20px;
}
</style>
@@ -1,148 +0,0 @@
<template>
<div class="settings-section">
<b>Cluster Icon</b>
<p>
Define cluster icon. By default automatically generated.
</p>
<div class="row">
<span class="cluster-settings-icon">
<img v-if="preferences.icon" :src="preferences.icon" class="cluster-icon">
<hashicon v-else :name="cluster.preferences.clusterName" size="38" />
</span>
<div class="col">
<b-form-file
ref="fileUpload"
v-model="file"
accept="image/jpeg, image/png, image/gif"
:state="Boolean(file)"
placeholder="Browse for new icon..."
drop-placeholder="Drop file here..."
browse-text=""
@input="upload"
/>
</div>
</div>
<div class="mt-3">
<a href="#" @click.prevent="reset" v-if="preferences.icon" class="text-muted">
Use automatically generated icon
</a>
<b-alert show variant="danger" v-if="status === 'ERROR'">
{{ errorMsg }}
</b-alert>
</div>
</div>
</template>
<script>
import hashicon from "../../hashicon/hashicon";
export default {
name: 'ClusterIcon',
components: {
hashicon
},
props: {
cluster: {
type: Object,
required: true,
default: null
}
},
data(){
return {
status: "",
errorMsg: "",
file: null,
}
},
computed:{
preferences: function() {
return this.cluster.preferences;
},
isProcessing: function() {
return this.status === "PROCESSING";
},
isResetProcessing: function() {
return this.resetStatus === "PROCESSING";
},
},
methods: {
reset: async function(){
this.resetStatus = "PROCESSING";
let error = null;
let result = await this.$store.dispatch("resetClusterIcon", {
clusterId: this.cluster.id
}).catch(e => {
error = e;
return false; // just returning false to promise
})
// handle exceptions here; but oh why, why do we have to???
if(error){
this.resetStatus = "ERROR";
this.errorMsg = error;
return false;
}
// now let's look at result...
if(result.success === false) {
this.resetStatus = "ERROR"
this.errorMsg = result.message
} else {
this.resetStatus = "";
this.errorMsg = "";
}
},
upload: async function(file){
if(!file) {
return
}
this.status="PROCESSING";
let error = null;
let result = await this.$store.dispatch("uploadClusterIcon", {
clusterId: this.cluster.id,
name: file.name,
path: file.path
}).catch(e => {
error = e;
return false; // just returning false to promise
})
// handle exceptions here; but oh why, why do we have to???
if(error){
this.status = "ERROR";
this.errorMsg = error;
return false;
}
// now let's look at result...
if(result.success === false) {
this.status = "ERROR"
this.errorMsg = result.message
} else {
this.status = "";
this.errorMsg = "";
this.file = null;
}
},
},
mounted: function(){
}
}
</script>
<style scoped lang="scss">
.description{
padding: 0.75rem;
}
.actions{
border-top: 1px solid rgba(255,255,255,0.10);
padding: 15px;
}
.cluster-settings-icon {
background: #252729;
margin-left: 15px;
padding: 5px;
}
</style>
@@ -1,49 +0,0 @@
<template>
<div class="settings-section">
<b>Cluster Name</b>
<p>
Define cluster name.
</p>
<b-form-input v-model="cluster.preferences.clusterName"
:placeholder="cluster.preferences.clusterName"
:state="errors.name"
trim
@blur="onSave"
/>
</div>
</template>
<script>
export default {
name: 'ClusterName',
props: {
cluster: {
type: Object,
required: true,
default: null
}
},
data(){
return {
errors: {
name: null
}
}
},
methods: {
onSave: function() {
if(this.cluster.preferences.clusterName === "") {
this.errors.name = false
return
} else {
this.errors.name = true
}
this.$store.dispatch("storeCluster", this.cluster);
}
}
}
</script>
<style>
</style>
@@ -1,52 +0,0 @@
<template>
<div class="settings-section">
<b>Cluster Workspace</b>
<p>
Define cluster <a href="/#/workspaces">workspace</a>.
</p>
<b-form-select
v-model="cluster.workspace"
:options="workspaces"
@change="onSave"
/>
</div>
</template>
<script>
export default {
name: 'ClusterWorkspace',
props: {
cluster: {
type: Object,
required: true,
default: null
}
},
computed: {
workspaces: function() {
return this.$store.getters.workspaces.map((ws) => {
return {
value: ws.id,
text: ws.name
}
});
}
},
methods: {
onSave: async function(workspaceId) {
this.cluster.workspace = workspaceId
await this.$store.dispatch("storeCluster", this.cluster);
const ws = this.$store.getters.workspaceById(this.cluster.workspace);
if (ws) {
this.$store.commit("setCurrentWorkspace", ws);
await this.$store.dispatch("clearClusters");
await this.$store.dispatch("refreshClusters", ws);
}
}
}
}
</script>
<style>
</style>
@@ -1,35 +0,0 @@
<template>
<div id="cluster-general" class="row">
<div class="col-12">
<h2>General</h2>
<cluster-name :cluster="cluster" />
<cluster-workspace :cluster="cluster" />
<cluster-icon :cluster="cluster" />
</div>
</div>
</template>
<script>
import ClusterIcon from "./ClusterIcon";
import ClusterName from "./ClusterName"
import ClusterWorkspace from "./ClusterWorkspace"
export default {
name: "ClusterSettingsGeneral",
props: {
cluster: {
type: Object,
required: true,
default: null
}
},
components: {
ClusterIcon,
ClusterName,
ClusterWorkspace
}
}
</script>
<style>
</style>
@@ -1,69 +0,0 @@
<template>
<div id="cluster-status" class="cluster-settings-overview row">
<div class="col-12">
<h2>Status</h2>
<div class="settings-section">
<b>Cluster Status</b>
<p>
Cluster status information including detected distribution, kernel version and online status
</p>
<table class="table">
<tbody>
<tr>
<th scope="row">
Online Status
</th>
<td>{{ cluster.online ? "online" : "offline (" + (cluster.failureReason || "unknown reason") + ")" }}</td>
</tr>
<tr>
<th scope="row">
Distribution
</th>
<td>{{ cluster.distribution }}</td>
</tr>
<tr>
<th scope="row">
Kernel Version
</th>
<td>{{ cluster.version }}</td>
</tr>
<tr>
<th scope="row">
API Address
</th>
<td>{{ cluster.apiUrl }}</td>
</tr>
<tr v-if="cluster.nodes">
<th scope="row">
Nodes Count
</th>
<td>{{ cluster.nodes }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ClusterSettingsOverview',
props: {
cluster: {
type: Object,
default: null,
}
},
data(){
return {
}
},
methods: {
}
}
</script>
<style lang="scss">
</style>
Loaded 100 of 807 files, more files were not shown because too many files have changed in this diff. Show more