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:
parent
6412d73a5a
commit
5c1974f07b
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user