diff --git a/dashboard/client/api/endpoints/endpoint.api.ts b/dashboard/client/api/endpoints/endpoint.api.ts index e01c86072a..6f0c400969 100644 --- a/dashboard/client/api/endpoints/endpoint.api.ts +++ b/dashboard/client/api/endpoints/endpoint.api.ts @@ -86,6 +86,9 @@ export class EndpointSubset implements IEndpointSubset { return "" } return this.addresses.map(address => { + if (!this.ports) { + return address.ip + } return this.ports.map(port => { return `${address.ip}:${port.port}` }).join(", ") diff --git a/dashboard/client/api/endpoints/helm-releases.api.ts b/dashboard/client/api/endpoints/helm-releases.api.ts index 23ea9653c1..dfd284905f 100644 --- a/dashboard/client/api/endpoints/helm-releases.api.ts +++ b/dashboard/client/api/endpoints/helm-releases.api.ts @@ -170,7 +170,12 @@ export class HelmRelease implements ItemObject { } getVersion() { - return this.chart.match(/(\d+)[^-]*$/)[0]; + const versions = this.chart.match(/(\d+)[^-]*$/) + if (versions) { + return versions[0] + } else { + return "" + } } getUpdated(humanize = true, compact = true) { diff --git a/spec/src/main/port_spec.ts b/spec/src/main/port_spec.ts index 58c62c73b0..8c7566e02c 100644 --- a/spec/src/main/port_spec.ts +++ b/spec/src/main/port_spec.ts @@ -2,13 +2,10 @@ import { EventEmitter } from 'events' class MockServer extends EventEmitter { listen = jest.fn((obj) => { - if(obj.port < 9003) { - this.emit('error', new Error("fail!")) - } else { - this.emit('listening', {}) - } + this.emit('listening', {}) return this }) + address = () => { return { port: 12345 }} unref = jest.fn() close = jest.fn((cb) => { cb() @@ -29,11 +26,7 @@ describe("getFreePort", () => { jest.clearAllMocks() }) - it("fails for an invalid range", async () => { - return expect(port.getFreePort(1, 2)).rejects.toMatch('free port') - }) - it("finds the next free port", async () => { - return expect(port.getFreePort(9000, 9005)).resolves.toBe(9003) + return expect(port.getFreePort()).resolves.toEqual(expect.any(Number)) }) }) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index bbae37d96c..cb900e898a 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -96,7 +96,6 @@ export class Cluster implements ClusterInfo { this.contextName = kc.currentContext this.url = this.contextHandler.url this.apiUrl = kc.getCurrentCluster().server - await this.contextHandler.init() } public stopServer() { diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index 431f8c450a..9f2977dfc4 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -1,4 +1,4 @@ -import { KubeConfig } from "@kubernetes/client-node" +import { KubeConfig, CoreV1Api } from "@kubernetes/client-node" import { readFileSync } from "fs" import * as http from "http" import { ServerOptions } from "http-proxy" @@ -7,6 +7,9 @@ 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 @@ -28,6 +31,7 @@ export class ContextHandler { protected defaultNamespace: string protected proxyPort: number protected kubernetesApi: string + protected prometheusProvider: string protected prometheusPath: string protected clusterName: string @@ -56,7 +60,6 @@ export class ContextHandler { 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.setClusterPreferences(cluster.preferences) this.kc.clusters = [ { name: kc.getCurrentCluster().name, @@ -64,14 +67,17 @@ export class ContextHandler { 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 = "lens-metrics/services/prometheus:80" + this.prometheusPath = null } if(clusterPreferences && clusterPreferences.clusterName) { this.clusterName = clusterPreferences.clusterName; @@ -80,28 +86,48 @@ export class ContextHandler { } } - public getPrometheusPath() { - return this.prometheusPath + protected async resolvePrometheusPath(): Promise { + const service = await this.getPrometheusService() + return `${service.namespace}/services/${service.service}:${service.port}` } - public async init() { - const currentCluster = this.kc.getCurrentCluster() - if (currentCluster.caFile) { - this.certData = readFileSync(currentCluster.caFile).toString() - } else if (currentCluster.caData) { - this.certData = Buffer.from(currentCluster.caData, "base64").toString("ascii") + public async getPrometheusProvider() { + if (!this.prometheusProvider) { + const service = await this.getPrometheusService() + logger.info(`using ${service.id} as prometheus provider`) + this.prometheusProvider = service.id } - const user = this.kc.getCurrentUser() - if (user.authProvider && user.authProvider.name === "oidc") { - const authConfig = user.authProvider.config - if (authConfig["idp-certificate-authority"]) { - this.authCertData = readFileSync(authConfig["idp-certificate-authority"]).toString() - } else if (authConfig["idp-certificate-authority-data"]) { - this.authCertData = Buffer.from(authConfig["idp-certificate-authority-data"], "base64").toString("ascii") + return prometheusProviders.find(p => p.id === this.prometheusProvider) + } + + public async getPrometheusService(): Promise { + const providers = this.prometheusProvider ? prometheusProviders.filter((p, _) => p.id == this.prometheusProvider) : prometheusProviders + const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { + 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 { + 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 @@ -135,7 +161,7 @@ export class ContextHandler { let serverPort: number = null try { - serverPort = await getFreePort(49901, 65535) + serverPort = await getFreePort() } catch(error) { logger.error(error) throw(error) diff --git a/src/main/index.ts b/src/main/index.ts index 700b44a4e3..8bf99ca583 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -48,7 +48,7 @@ async function main() { let port: number = null // find free port try { - port = await getFreePort(49152, 65535) + port = await getFreePort() } catch (error) { logger.error(error) await dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy") diff --git a/src/main/port.ts b/src/main/port.ts index 6240575ee1..f419f4e78c 100644 --- a/src/main/port.ts +++ b/src/main/port.ts @@ -1,27 +1,30 @@ import logger from "./logger" import { createServer } from "net" +import { AddressInfo } from "net" -// Adapted from https://gist.github.com/mikeal/1840641#gistcomment-2896667 -function checkPort(port: number) { +const getNextAvailablePort = () => { + logger.debug("getNextAvailablePort() start") const server = createServer() server.unref() - return new Promise((resolve, reject) => + return new Promise((resolve, reject) => server - .on('error', error => reject(error)) - .on('listening', () => server.close(() => resolve(port))) - .listen({host: "127.0.0.1", port: port})) + .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 async function getFreePort(firstPort: number, lastPort: number): Promise { - let port = firstPort - - while(true) { - try { - logger.debug("Checking port " + port + " availability ...") - await checkPort(port) - return(port) - } catch(error) { - if(++port > lastPort) throw("Could not find a free port") - } +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 } diff --git a/src/main/prometheus/helm.ts b/src/main/prometheus/helm.ts index c69df755d5..f1462931df 100644 --- a/src/main/prometheus/helm.ts +++ b/src/main/prometheus/helm.ts @@ -1,10 +1,29 @@ import { PrometheusLens } from "./lens" +import { CoreV1Api } from "@kubernetes/client-node" +import { PrometheusService } from "./provider-registry"; +import logger from "../logger" export class PrometheusHelm extends PrometheusLens { - constructor() { - super() - this.id = "helm" - this.name = "Helm" - this.rateAccuracy = "5m" + id = "helm" + name = "Helm" + rateAccuracy = "5m" + + public async getPrometheusService(client: CoreV1Api): Promise { + 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 + } } -} \ No newline at end of file +} diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens.ts index f278000731..0b16f925e0 100644 --- a/src/main/prometheus/lens.ts +++ b/src/main/prometheus/lens.ts @@ -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 { id = "lens" name = "Lens" rateAccuracy = "1m" - public getQueries(opts: PrometheusQueryOpts): PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery { + public async getPrometheusService(client: CoreV1Api): Promise { + 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 { @@ -63,4 +80,4 @@ export class PrometheusLens implements PrometheusProvider { } } } -} \ No newline at end of file +} diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts index a44abbe72f..1da01125a2 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator.ts @@ -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 { rateAccuracy = "1m" id = "operator" name = "Prometheus Operator" - public getQueries(opts: PrometheusQueryOpts): PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery { + public async getPrometheusService(client: CoreV1Api): Promise { + 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 { @@ -63,4 +88,4 @@ export class PrometheusOperator implements PrometheusProvider { } } } -} \ No newline at end of file +} diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts index d02feb1ab5..c59b2ad8ea 100644 --- a/src/main/prometheus/provider-registry.ts +++ b/src/main/prometheus/provider-registry.ts @@ -1,3 +1,5 @@ +import { CoreV1Api } from "@kubernetes/client-node" + export type PrometheusClusterQuery = { memoryUsage: string; memoryRequests: string; @@ -48,8 +50,20 @@ 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 { - getQueries(opts: PrometheusQueryOpts): PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery; + id: string; + name: string; + getQueries(opts: PrometheusQueryOpts): PrometheusQuery; + getPrometheusService(client: CoreV1Api): Promise; } export type PrometheusProviderList = { @@ -73,4 +87,4 @@ export class PrometheusProviderRegistry { static getProviders(): PrometheusProvider[] { return Object.values(this.prometheusProviders) } -} \ No newline at end of file +} diff --git a/src/main/proxy.ts b/src/main/proxy.ts index ff998d79dd..538a960702 100644 --- a/src/main/proxy.ts +++ b/src/main/proxy.ts @@ -83,8 +83,6 @@ export class LensProxy { }, (250 * retryCount)) } } - - //return } res.writeHead(500, { 'Content-Type': 'text/plain' diff --git a/src/main/routes/metrics.ts b/src/main/routes/metrics.ts index e9f27907bb..289698a835 100644 --- a/src/main/routes/metrics.ts +++ b/src/main/routes/metrics.ts @@ -13,7 +13,6 @@ class MetricsRoute extends LensApi { const { response, cluster} = request const query: MetricsQuery = request.payload; const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube` - const metricsUrl = `${serverUrl}/api/v1/namespaces/${cluster.contextHandler.getPrometheusPath()}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` const headers = { "Host": `${cluster.id}.localhost:${cluster.port}`, "Content-type": "application/json", @@ -23,10 +22,12 @@ class MetricsRoute extends LensApi { queryParams[key] = value }) - const prometheusInstallationSource = cluster.preferences.prometheusProvider?.type || "lens" + let metricsUrl: string let prometheusProvider: PrometheusProvider try { - prometheusProvider = PrometheusProviderRegistry.getProvider(prometheusInstallationSource) + 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 diff --git a/src/main/routes/port-forward.ts b/src/main/routes/port-forward.ts index f79cc9228b..4b0a4077f0 100644 --- a/src/main/routes/port-forward.ts +++ b/src/main/routes/port-forward.ts @@ -36,7 +36,7 @@ class PortForward { } public async start() { - this.localPort = await getFreePort(8000, 9999) + this.localPort = await getFreePort() const kubectlBin = await bundledKubectl.kubectlPath() const args = [ "--kubeconfig", this.kubeConfig, diff --git a/src/renderer/components/ClusterSettings/Preferences/index.vue b/src/renderer/components/ClusterSettings/Preferences/index.vue index 096e648553..00753e9ff7 100644 --- a/src/renderer/components/ClusterSettings/Preferences/index.vue +++ b/src/renderer/components/ClusterSettings/Preferences/index.vue @@ -20,17 +20,7 @@
Prometheus

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

- - - + + + +
@@ -86,10 +88,23 @@ export default { prometheusProvider: "", } }, + 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() { - this.prometheusProviders = prometheusProviders.map((provider) => { - return { text: provider.name, value: provider.id } - }) this.updateValues() }, methods: { @@ -103,7 +118,7 @@ export default { if (this.cluster.preferences.prometheusProvider) { this.prometheusProvider = this.cluster.preferences.prometheusProvider.type } else { - this.prometheusProvider = "lens" + this.prometheusProvider = "" } }, parsePrometheusPath: function(path) {