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

Merge branch 'master' into feature/prometheus-api-prefix

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
Lauri Nevala 2020-05-06 20:05:03 +03:00
commit 85f2f43cf7
15 changed files with 202 additions and 84 deletions

View File

@ -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(", ")

View File

@ -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) {

View File

@ -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))
})
})

View File

@ -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() {

View File

@ -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<string> {
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<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
@ -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)

View File

@ -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")

View File

@ -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<number>((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<number> {
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
}

View File

@ -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<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 {
id = "lens"
name = "Lens"
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) {
case 'cluster':
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 {
rateAccuracy = "1m"
id = "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) {
case 'cluster':
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 = {
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<PrometheusService>;
}
export type PrometheusProviderList = {
@ -73,4 +87,4 @@ export class PrometheusProviderRegistry {
static getProviders(): PrometheusProvider[] {
return Object.values(this.prometheusProviders)
}
}
}

View File

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

View File

@ -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

View File

@ -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,

View File

@ -20,17 +20,7 @@
<div class="cluster-settings-section">
<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>
<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
label="Prometheus installation method."
description="What query format is used to fetch metrics from Prometheus"
@ -41,6 +31,18 @@
@change="onPrometheusProviderSave"
/>
</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 class="col-12">
@ -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) {