1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Auto-detect prometheus installation (#343)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2020-05-06 18:55:49 +03:00 committed by GitHub
parent 6412d73a5a
commit 5c1974f07b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 167 additions and 54 deletions

View File

@ -95,7 +95,6 @@ export class Cluster implements ClusterInfo {
this.contextName = kc.currentContext this.contextName = kc.currentContext
this.url = this.contextHandler.url this.url = this.contextHandler.url
this.apiUrl = kc.getCurrentCluster().server this.apiUrl = kc.getCurrentCluster().server
await this.contextHandler.init()
} }
public stopServer() { public stopServer() {

View File

@ -1,4 +1,4 @@
import { KubeConfig } from "@kubernetes/client-node" import { KubeConfig, CoreV1Api } from "@kubernetes/client-node"
import { readFileSync } from "fs" import { readFileSync } from "fs"
import * as http from "http" import * as http from "http"
import { ServerOptions } from "http-proxy" import { ServerOptions } from "http-proxy"
@ -7,6 +7,9 @@ import logger from "./logger"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy" import { KubeAuthProxy } from "./kube-auth-proxy"
import { Cluster, ClusterPreferences } from "./cluster" 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 { export class ContextHandler {
public contextName: string public contextName: string
@ -28,6 +31,7 @@ export class ContextHandler {
protected defaultNamespace: string protected defaultNamespace: string
protected proxyPort: number protected proxyPort: number
protected kubernetesApi: string protected kubernetesApi: string
protected prometheusProvider: string
protected prometheusPath: string protected prometheusPath: string
protected clusterName: string protected clusterName: string
@ -56,7 +60,6 @@ export class ContextHandler {
this.defaultNamespace = kc.getContextObject(kc.currentContext).namespace this.defaultNamespace = kc.getContextObject(kc.currentContext).namespace
this.url = `http://${this.id}.localhost:${cluster.port}/` this.url = `http://${this.id}.localhost:${cluster.port}/`
this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}` this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}`
this.setClusterPreferences(cluster.preferences)
this.kc.clusters = [ this.kc.clusters = [
{ {
name: kc.getCurrentCluster().name, name: kc.getCurrentCluster().name,
@ -64,14 +67,17 @@ export class ContextHandler {
skipTLSVerify: true skipTLSVerify: true
} }
] ]
this.setClusterPreferences(cluster.preferences)
} }
public setClusterPreferences(clusterPreferences?: ClusterPreferences) { public setClusterPreferences(clusterPreferences?: ClusterPreferences) {
this.prometheusProvider = clusterPreferences.prometheusProvider?.type
if (clusterPreferences && clusterPreferences.prometheus) { if (clusterPreferences && clusterPreferences.prometheus) {
const prom = clusterPreferences.prometheus const prom = clusterPreferences.prometheus
this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}` this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}`
} else { } else {
this.prometheusPath = "lens-metrics/services/prometheus:80" this.prometheusPath = null
} }
if(clusterPreferences && clusterPreferences.clusterName) { if(clusterPreferences && clusterPreferences.clusterName) {
this.clusterName = clusterPreferences.clusterName; this.clusterName = clusterPreferences.clusterName;
@ -80,28 +86,48 @@ export class ContextHandler {
} }
} }
public getPrometheusPath() { protected async resolvePrometheusPath(): Promise<string> {
return this.prometheusPath const service = await this.getPrometheusService()
return `${service.namespace}/services/${service.service}:${service.port}`
} }
public async init() { public async getPrometheusProvider() {
const currentCluster = this.kc.getCurrentCluster() if (!this.prometheusProvider) {
if (currentCluster.caFile) { const service = await this.getPrometheusService()
this.certData = readFileSync(currentCluster.caFile).toString() logger.info(`using ${service.id} as prometheus provider`)
} else if (currentCluster.caData) { this.prometheusProvider = service.id
this.certData = Buffer.from(currentCluster.caData, "base64").toString("ascii")
} }
const user = this.kc.getCurrentUser() return prometheusProviders.find(p => p.id === this.prometheusProvider)
if (user.authProvider && user.authProvider.name === "oidc") { }
const authConfig = user.authProvider.config
if (authConfig["idp-certificate-authority"]) { public async getPrometheusService(): Promise<PrometheusService> {
this.authCertData = readFileSync(authConfig["idp-certificate-authority"]).toString() const providers = this.prometheusProvider ? prometheusProviders.filter((p, _) => p.id == this.prometheusProvider) : prometheusProviders
} else if (authConfig["idp-certificate-authority-data"]) { const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
this.authCertData = Buffer.from(authConfig["idp-certificate-authority-data"], "base64").toString("ascii") 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) { public async getApiTarget(isWatchRequest = false) {
if (this.apiTarget && !isWatchRequest) { if (this.apiTarget && !isWatchRequest) {
return this.apiTarget return this.apiTarget

View File

@ -1,10 +1,29 @@
import { PrometheusLens } from "./lens" import { PrometheusLens } from "./lens"
import { CoreV1Api } from "@kubernetes/client-node"
import { PrometheusService } from "./provider-registry";
import logger from "../logger"
export class PrometheusHelm extends PrometheusLens { export class PrometheusHelm extends PrometheusLens {
constructor() { id = "helm"
super() name = "Helm"
this.id = "helm" rateAccuracy = "5m"
this.name = "Helm"
this.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
}
} }
} }

View File

@ -1,11 +1,28 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusClusterQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery } from "./provider-registry"; import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"
export class PrometheusLens implements PrometheusProvider { export class PrometheusLens implements PrometheusProvider {
id = "lens" id = "lens"
name = "Lens" name = "Lens"
rateAccuracy = "1m" rateAccuracy = "1m"
public getQueries(opts: PrometheusQueryOpts): PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery { 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) { switch(opts.category) {
case 'cluster': case 'cluster':
return { return {
@ -63,4 +80,4 @@ export class PrometheusLens implements PrometheusProvider {
} }
} }
} }
} }

View File

@ -1,11 +1,36 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusClusterQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery } from "./provider-registry"; import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api, V1Service } from "@kubernetes/client-node";
import logger from "../logger";
export class PrometheusOperator implements PrometheusProvider { export class PrometheusOperator implements PrometheusProvider {
rateAccuracy = "1m" rateAccuracy = "1m"
id = "operator" id = "operator"
name = "Prometheus Operator" name = "Prometheus Operator"
public getQueries(opts: PrometheusQueryOpts): PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery { 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) { switch(opts.category) {
case 'cluster': case 'cluster':
return { return {
@ -63,4 +88,4 @@ export class PrometheusOperator implements PrometheusProvider {
} }
} }
} }
} }

View File

@ -1,3 +1,5 @@
import { CoreV1Api } from "@kubernetes/client-node"
export type PrometheusClusterQuery = { export type PrometheusClusterQuery = {
memoryUsage: string; memoryUsage: string;
memoryRequests: string; memoryRequests: string;
@ -48,8 +50,20 @@ export type PrometheusQueryOpts = {
[key: string]: string | any; [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 { export interface PrometheusProvider {
getQueries(opts: PrometheusQueryOpts): PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery; id: string;
name: string;
getQueries(opts: PrometheusQueryOpts): PrometheusQuery;
getPrometheusService(client: CoreV1Api): Promise<PrometheusService>;
} }
export type PrometheusProviderList = { export type PrometheusProviderList = {
@ -73,4 +87,4 @@ export class PrometheusProviderRegistry {
static getProviders(): PrometheusProvider[] { static getProviders(): PrometheusProvider[] {
return Object.values(this.prometheusProviders) return Object.values(this.prometheusProviders)
} }
} }

View File

@ -83,8 +83,6 @@ export class LensProxy {
}, (250 * retryCount)) }, (250 * retryCount))
} }
} }
//return
} }
res.writeHead(500, { res.writeHead(500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'

View File

@ -13,7 +13,6 @@ class MetricsRoute extends LensApi {
const { response, cluster} = request const { response, cluster} = request
const query: MetricsQuery = request.payload; const query: MetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube` const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
const metricsUrl = `${serverUrl}/api/v1/namespaces/${cluster.contextHandler.getPrometheusPath()}/proxy/api/v1/query_range`
const headers = { const headers = {
"Host": `${cluster.id}.localhost:${cluster.port}`, "Host": `${cluster.id}.localhost:${cluster.port}`,
"Content-type": "application/json", "Content-type": "application/json",
@ -23,10 +22,12 @@ class MetricsRoute extends LensApi {
queryParams[key] = value queryParams[key] = value
}) })
const prometheusInstallationSource = cluster.preferences.prometheusProvider?.type || "lens" let metricsUrl: string
let prometheusProvider: PrometheusProvider let prometheusProvider: PrometheusProvider
try { try {
prometheusProvider = PrometheusProviderRegistry.getProvider(prometheusInstallationSource) const prometheusPath = await cluster.contextHandler.getPrometheusPath()
metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy/api/v1/query_range`
prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
} catch { } catch {
this.respondJson(response, {}) this.respondJson(response, {})
return return

View File

@ -20,17 +20,7 @@
<div class="cluster-settings-section"> <div class="cluster-settings-section">
<b>Prometheus</b> <b>Prometheus</b>
<p>Use pre-installed Prometheus service for metrics. Please refer to the <a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md">guide</a> for possible configuration changes.</p> <p>Use pre-installed Prometheus service for metrics. Please refer to the <a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md">guide</a> for possible configuration changes.</p>
<b-form-group
label="Prometheus service address."
description="A path to an existing Prometheus installation (<namespace>/<service>:<port>)."
>
<b-form-input
v-model="prometheusPath"
placeholder="lens-metrics/prometheus:80"
id="input-prometheuspath"
@blur="onPrometheusSave"
/>
</b-form-group>
<b-form-group <b-form-group
label="Prometheus installation method." label="Prometheus installation method."
description="What query format is used to fetch metrics from Prometheus" description="What query format is used to fetch metrics from Prometheus"
@ -41,6 +31,18 @@
@change="onPrometheusProviderSave" @change="onPrometheusProviderSave"
/> />
</b-form-group> </b-form-group>
<b-form-group
label="Prometheus service address."
description="An address to an existing Prometheus installation (<namespace>/<service>:<port>). Lens tries to auto-detect address if left empty."
v-if="canEditPrometheusPath"
>
<b-form-input
v-model="prometheusPath"
placeholder="<namespace>/<service>:<port>"
id="input-prometheuspath"
@blur="onPrometheusSave"
/>
</b-form-group>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
@ -84,13 +86,25 @@ export default {
}, },
prometheusPath: "", prometheusPath: "",
prometheusProvider: "", prometheusProvider: "",
prometheusProviders: [], }
},
computed: {
prometheusProviders: function() {
const providers = prometheusProviders.map((provider) => {
return { text: provider.name, value: provider.id }
})
providers.unshift({text: "Auto detect", value: ""})
return providers;
},
canEditPrometheusPath: function() {
if (this.prometheusProvider === "") return false
if (this.prometheusProvider === "lens") return false
return true
} }
}, },
mounted: async function() { mounted: async function() {
this.prometheusProviders = prometheusProviders.map((provider) => {
return { text: provider.name, value: provider.id }
})
this.updateValues() this.updateValues()
}, },
methods: { methods: {
@ -104,7 +118,7 @@ export default {
if (this.cluster.preferences.prometheusProvider) { if (this.cluster.preferences.prometheusProvider) {
this.prometheusProvider = this.cluster.preferences.prometheusProvider.type this.prometheusProvider = this.cluster.preferences.prometheusProvider.type
} else { } else {
this.prometheusProvider = "lens" this.prometheusProvider = ""
} }
}, },
parsePrometheusPath: function(path) { parsePrometheusPath: function(path) {