Skip to content

Commit 006252c

Browse files
committed
Merge branch 'prometheus_port_detection'
2 parents c677ea6 + ab70f9d commit 006252c

File tree

2 files changed

+247
-11
lines changed

2 files changed

+247
-11
lines changed

prometheus/src/request.tsx

Lines changed: 241 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,255 @@ import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
22

33
const 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.

prometheus/src/util.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConfigStore } from '@kinvolk/headlamp-plugin/lib';
2-
import { isPrometheusInstalled } from './request';
2+
import { isPrometheusInstalled, KubernetesType } from './request';
33

44
export const PLUGIN_NAME = 'prometheus';
55

@@ -101,13 +101,14 @@ export async function getPrometheusPrefix(cluster: string): Promise<string | nul
101101
// if so return the prometheus pod address
102102
const clusterData = getClusterConfig(cluster);
103103
if (clusterData?.autoDetect) {
104-
const [isInstalled, prometheusPodName, prometheusPodNamespace] = await isPrometheusInstalled();
105-
if (isInstalled) {
106-
return `${prometheusPodNamespace}/pods/${prometheusPodName}`;
107-
} else {
104+
const prometheusEndpoint = await isPrometheusInstalled();
105+
if (prometheusEndpoint.type === KubernetesType.none) {
108106
return null;
109107
}
108+
const prometheusPortStr = prometheusEndpoint.port ? `:${prometheusEndpoint.port}` : '';
109+
return `${prometheusEndpoint.namespace}/${prometheusEndpoint.type}/${prometheusEndpoint.name}${prometheusPortStr}`;
110110
}
111+
111112
if (clusterData?.address) {
112113
const [namespace, service] = clusterData?.address.split('/');
113114
return `${namespace}/services/${service}`;

0 commit comments

Comments
 (0)