@@ -2,20 +2,255 @@ import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
22
33const request = ApiProxy . request ;
44
5- export async function isPrometheusInstalled ( ) {
5+ const CUSTOM_HEADLAMP_LABEL = 'headlamp-prometheus=true' ;
6+ const COMMON_PROMETHEUS_POD_LABEL = 'app.kubernetes.io/name=prometheus' ;
7+ const COMMON_PROMETHEUS_SERVICE_LABEL = 'app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server' ;
8+ const DEFAULT_PROMETHEUS_PORT = '9090' ;
9+
10+ export type KubernetesPodListResponseItem = {
11+ metadata : {
12+ name : string ;
13+ namespace : string ;
14+ } ;
15+ spec : {
16+ containers : [
17+ {
18+ name : string ;
19+ image : string ;
20+ ports : [
21+ {
22+ name : string ;
23+ containerPort : number ;
24+ protocol : string ;
25+ }
26+ ] ;
27+ }
28+ ] ;
29+ } ;
30+ } ;
31+
32+ export type KubernetesPodListResponse = {
33+ kind : 'PodList' ;
34+ items : KubernetesPodListResponseItem [ ] ;
35+ } ;
36+
37+ export type KubernetesServiceListResponseItem = {
38+ metadata : {
39+ name : string ;
40+ namespace : string ;
41+ } ;
42+ spec : {
43+ ports : [
44+ {
45+ name : string ;
46+ port : number ;
47+ protocol : string ;
48+ }
49+ ] ;
50+ } ;
51+ } ;
52+
53+ export type KubernetesServiceListResponse = {
54+ kind : 'ServiceList' ;
55+ items : KubernetesServiceListResponseItem [ ] ;
56+ } ;
57+
58+ export type KubernetesSearchResponse = KubernetesPodListResponse | KubernetesServiceListResponse ;
59+
60+ export enum KubernetesType {
61+ none = 'none' ,
62+ pods = 'pods' ,
63+ services = 'services' ,
64+ } ;
65+
66+ export type PrometheusEndpoint = {
67+ type : KubernetesType ;
68+ name : string | undefined ;
69+ namespace : string | undefined ;
70+ port : string | undefined ;
71+ } ;
72+
73+ /**
74+ * Helper to create a new instance of PrometheusEndpoint.
75+ * @param {KubernetesType } type - The type of Kubernetes resource.
76+ * @param {string } name - The name of the Kubernetes resource.
77+ * @param {string } namespace - The namespace of the Kubernetes resource.
78+ * @param {string } port - The port of the Kubernetes resource.
79+ * @returns {PrometheusEndpoint } - A new instance of PrometheusEndpoint.
80+ */
81+ export function createPrometheusEndpoint (
82+ type : KubernetesType = KubernetesType . none ,
83+ name : string | undefined = undefined ,
84+ namespace : string | undefined = undefined ,
85+ port : string | undefined = undefined
86+ ) : PrometheusEndpoint {
87+ return {
88+ type,
89+ name,
90+ namespace,
91+ port
92+ } ;
93+ }
94+
95+ /**
96+ * Returns the first Prometheus pod or service that fits our search and is reachable.
97+ * @returns {Promise<PrometheusEndpoint> } - A promise that resolves to the first reachable Prometheus pod/service or none if none are reachable.
98+ */
99+ export async function isPrometheusInstalled ( ) : Promise < PrometheusEndpoint > {
100+ // Search by a custom label for a pod
101+ const podSearchSpecificResponse = await searchKubernetesByLabel ( KubernetesType . pods , CUSTOM_HEADLAMP_LABEL ) ;
102+ if ( podSearchSpecificResponse . type !== KubernetesType . none ) {
103+ return podSearchSpecificResponse ;
104+ }
105+
106+ // Search by a custom label for a service
107+ const serviceSearchSpecificResponse = await searchKubernetesByLabel ( KubernetesType . services , CUSTOM_HEADLAMP_LABEL ) ;
108+ if ( serviceSearchSpecificResponse . type !== KubernetesType . none ) {
109+ return serviceSearchSpecificResponse ;
110+ }
111+
112+ // Search by common label for a pod
113+ const podSearchResponse = await searchKubernetesByLabel ( KubernetesType . pods , COMMON_PROMETHEUS_POD_LABEL ) ;
114+ if ( podSearchResponse . type !== KubernetesType . none ) {
115+ return podSearchResponse ;
116+ }
117+
118+ // Search by common label for a service
119+ const serviceSearchResponse = await searchKubernetesByLabel ( KubernetesType . services , COMMON_PROMETHEUS_SERVICE_LABEL ) ;
120+ if ( serviceSearchResponse . type !== KubernetesType . none ) {
121+ return serviceSearchResponse ;
122+ }
123+
124+ // No Prometheus pod or service found
125+ return createPrometheusEndpoint ( ) ;
126+ }
127+
128+ /**
129+ * Searches for a Kubernetes resource by label and tests if Prometheus is reachable.
130+ * @param {KubernetesType } kubernetesType - The type of Kubernetes resource.
131+ * @param {string } labelSelector - The label selector to search for.
132+ * @returns {Promise<PrometheusEndpoint> } - A promise that resolves to the Prometheus endpoint or none if none are reachable.
133+ */
134+ async function searchKubernetesByLabel (
135+ kubernetesType : KubernetesType ,
136+ labelSelector : string
137+ ) : Promise < PrometheusEndpoint > {
138+ if ( kubernetesType === KubernetesType . none ) {
139+ return createPrometheusEndpoint ( ) ;
140+ }
141+
6142 const queryParams = new URLSearchParams ( ) ;
7- queryParams . append ( 'labelSelector' , 'app.kubernetes.io/name=prometheus' ) ;
143+ queryParams . append ( 'labelSelector' , labelSelector ) ;
8144
9- const response = await request ( `/api/v1/pods ?${ queryParams . toString ( ) } ` , {
145+ const searchResponse = await request ( `/api/v1/${ kubernetesType } ?${ queryParams } ` , {
10146 method : 'GET' ,
11147 } ) ;
12148
13- if ( response . items && response . items . length > 0 ) {
14- return [ true , response . items [ 0 ] . metadata . name , response . items [ 0 ] . metadata . namespace ] ;
149+ if ( ! searchResponse ?. kind || [ 'PodList' , 'ServiceList' ] . indexOf ( searchResponse . kind ) === - 1 ) {
150+ return createPrometheusEndpoint ( ) ;
151+ }
152+
153+ const searchResponseTyped = searchResponse as KubernetesSearchResponse ;
154+
155+ if ( searchResponseTyped . items ?. length > 0 ) {
156+ const metadata = searchResponseTyped . items [ 0 ] . metadata ;
157+ if ( ! metadata ) {
158+ return createPrometheusEndpoint ( ) ;
159+ }
160+
161+ const prometheusName = metadata . name ;
162+ const prometheusNamespace = metadata . namespace ;
163+ const prometheusPorts = getPrometheusPortsFromResponse ( searchResponseTyped ) ;
164+
165+ const testResults = await Promise . all (
166+ prometheusPorts . map ( async ( prometheusPort ) => {
167+ const testSuccess = await testPrometheusQuery ( kubernetesType , prometheusName , prometheusNamespace , prometheusPort ) ;
168+ return {
169+ prometheusPort,
170+ testSuccess
171+ } ;
172+ } )
173+ ) ;
174+
175+ for ( const result of testResults ) {
176+ if ( result . testSuccess ) {
177+ return createPrometheusEndpoint ( kubernetesType , prometheusName , prometheusNamespace , result . prometheusPort ) ;
178+ }
179+ }
15180 }
16- return [ false , null , null ] ;
181+
182+ return createPrometheusEndpoint ( ) ;
17183}
18184
185+ /**
186+ * Gets the Prometheus service port from the response.
187+ * @param response - A PodList or ServiceList response.
188+ * @returns {string[] } - The Prometheus service ports.
189+ */
190+ function getPrometheusPortsFromResponse ( response : KubernetesSearchResponse ) : string [ ] {
191+ const ports : string [ ] = [ ] ;
192+ if ( response . kind === 'PodList' ) {
193+ // Pod response
194+ for ( const item of response . items ) {
195+ for ( const container of item . spec . containers ) {
196+ for ( const port of container . ports ) {
197+ if ( port . protocol === 'TCP' ) {
198+ ports . push ( String ( port . containerPort ) ) ;
199+ }
200+ }
201+ }
202+ }
203+ } else if ( response . kind === 'ServiceList' ) {
204+ // Service response
205+ for ( const item of response . items ) {
206+ for ( const port of item . spec . ports ) {
207+ if ( port . protocol === 'TCP' ) {
208+ ports . push ( String ( port . port ) ) ;
209+ }
210+ }
211+ }
212+ }
213+
214+ if ( ports . length === 0 ) {
215+ // Add the default Prometheus port if no ports are found
216+ ports . push ( DEFAULT_PROMETHEUS_PORT ) ;
217+ }
218+
219+ return ports ;
220+ }
221+
222+ /**
223+ * Tests if prometheus will respond to a query.
224+ * @param {KubernetesType } kubernetesType - The type of Kubernetes resource.
225+ * @param {string } prometheusName - The name of the Prometheus pod or service.
226+ * @param {string } prometheusNamespace - The namespace of the Prometheus pod or service.
227+ * @param {string } prometheusPort - The port of the Prometheus pod or service.
228+ */
229+ async function testPrometheusQuery (
230+ kubernetesType : KubernetesType ,
231+ prometheusName : string ,
232+ prometheusNamespace : string ,
233+ prometheusPort : string
234+ ) : Promise < boolean > {
235+ const queryParams = new URLSearchParams ( ) ;
236+ queryParams . append ( 'query' , 'up' ) ;
237+ const start = Math . floor ( Date . now ( ) / 1000 ) ;
238+ const testSuccess = await fetchMetrics ( {
239+ prefix : `${ prometheusNamespace } /${ kubernetesType } /${ prometheusName } ${ prometheusPort ? `:${ prometheusPort } ` : '' } ` ,
240+ query : 'up' ,
241+ from : start - 86400 ,
242+ to : start ,
243+ step : 300 ,
244+ } ) . then ( ( ) => {
245+ return true ;
246+ } ) . catch ( ( ) => {
247+ return false ;
248+ } ) ;
249+
250+ return testSuccess ;
251+ }
252+
253+
19254/**
20255 * Fetches metrics data from Prometheus using the provided parameters.
21256 * @param {object } data - The parameters for fetching metrics.
0 commit comments