From dd8dc23514fe783c5245f4e69b8c29294d379192 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 5 May 2020 07:28:54 +0300 Subject: [PATCH 1/4] Return empty string if Helm release version is not detected (#342) Signed-off-by: Lauri Nevala --- dashboard/client/api/endpoints/helm-releases.api.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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) { From 0a3b5dae73caae0e2e0686d38c5c831e04218bb6 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 5 May 2020 07:31:08 +0300 Subject: [PATCH 2/4] Fix EndpointSubset.toString() to work without ports (#336) Signed-off-by: Lauri Nevala --- dashboard/client/api/endpoints/endpoint.api.ts | 3 +++ 1 file changed, 3 insertions(+) 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(", ") From 6412d73a5a420c8fef70af68ee8a4b6a040662a7 Mon Sep 17 00:00:00 2001 From: Alexis Deruelle Date: Tue, 5 May 2020 06:31:58 +0200 Subject: [PATCH 3/4] Fix port availability test (#333) * Let OS allocate port number Port availability might be tricky if some port is already in use on the 'all' interface '0.0.0.0'. The proposed solution is to let the OS allocate the port for us using 0 as port specifier : - first create a server instance to allocate a port number - save port number - close the server - return the port number to the caller This should be safe granted the OS doesn't reuse the port numbers on consecutive port allocations. see : - about Node.js Net module : https://nodejs.org/docs/latest-v12.x/api/net.html#net_server_listen_port_host_backlog_callback - about safety around reusing port number : https://unix.stackexchange.com/a/132524 Signed-off-by: Alexis Deruelle --- spec/src/main/port_spec.ts | 13 +++--------- src/main/context-handler.ts | 2 +- src/main/index.ts | 2 +- src/main/port.ts | 37 ++++++++++++++++++--------------- src/main/routes/port-forward.ts | 2 +- 5 files changed, 26 insertions(+), 30 deletions(-) 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/context-handler.ts b/src/main/context-handler.ts index 431f8c450a..817b684d00 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -135,7 +135,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/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, From 5c1974f07bfd9c8e04884a0d3aeca633b43544e2 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 6 May 2020 18:55:49 +0300 Subject: [PATCH 4/4] Auto-detect prometheus installation (#343) Signed-off-by: Jari Kolehmainen --- src/main/cluster.ts | 1 - src/main/context-handler.ts | 62 +++++++++++++------ src/main/prometheus/helm.ts | 31 ++++++++-- src/main/prometheus/lens.ts | 23 ++++++- src/main/prometheus/operator.ts | 31 +++++++++- src/main/prometheus/provider-registry.ts | 18 +++++- src/main/proxy.ts | 2 - src/main/routes/metrics.ts | 7 ++- .../ClusterSettings/Preferences/index.vue | 46 +++++++++----- 9 files changed, 167 insertions(+), 54 deletions(-) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index b6323c0a0d..1398bf2f7f 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -95,7 +95,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 817b684d00..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 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 b7b1a030b0..6bd965394d 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/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/api/v1/query_range` + prometheusProvider = await cluster.contextHandler.getPrometheusProvider() } catch { this.respondJson(response, {}) return diff --git a/src/renderer/components/ClusterSettings/Preferences/index.vue b/src/renderer/components/ClusterSettings/Preferences/index.vue index 108b6ad207..a94f0b59d8 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.

- - - + + + +
@@ -84,13 +86,25 @@ export default { }, prometheusPath: "", 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() { - this.prometheusProviders = prometheusProviders.map((provider) => { - return { text: provider.name, value: provider.id } - }) this.updateValues() }, methods: { @@ -104,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) {