Skip to content

Commit fbba288

Browse files
committed
MK8S-197 - Add PrometheusAuthProvider
1 parent 0c90d1a commit fbba288

File tree

6 files changed

+202
-6
lines changed

6 files changed

+202
-6
lines changed

ui/src/FederableApp.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { applyMiddleware, compose, createStore, Store } from 'redux';
66
import createSagaMiddleware from 'redux-saga';
77
import 'regenerator-runtime/runtime';
88
import App from './containers/App';
9+
import PrometheusAuthProvider from './containers/PrometheusAuthProvider';
910
import { authErrorAction } from './ducks/app/authError';
1011
import { setApiConfigAction } from './ducks/config';
1112
import { setHistory as setReduxHistory } from './ducks/history';
@@ -125,11 +126,13 @@ export default function FederableApp(props: FederatedAppProps) {
125126
>
126127
<Provider store={store}>
127128
<AppConfigProvider>
128-
<ToastProvider>
129-
<RouterWithBaseName>
130-
<App />
131-
</RouterWithBaseName>
132-
</ToastProvider>
129+
<PrometheusAuthProvider>
130+
<ToastProvider>
131+
<RouterWithBaseName>
132+
<App />
133+
</RouterWithBaseName>
134+
</ToastProvider>
135+
</PrometheusAuthProvider>
133136
</AppConfigProvider>
134137
</Provider>
135138
</ShellHooksProvider>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Loader } from '@scality/core-ui';
2+
import { ReactNode, useEffect, useState } from 'react';
3+
import { useAuth } from './PrivateRoute';
4+
import { useTypedSelector } from '../hooks';
5+
import { setHeaders } from '../services/prometheus/api';
6+
import { fetchPrometheusOidcEnabled } from '../services/prometheus/fetchOidcEnabled';
7+
8+
export default function PrometheusAuthProvider({
9+
children,
10+
}: {
11+
children: ReactNode;
12+
}) {
13+
const { userData } = useAuth();
14+
const api = useTypedSelector((state) => state.config.api);
15+
const [oidcEnabled, setOidcEnabled] = useState<boolean | null>(null);
16+
17+
// Fetch the OIDC config and set headers before rendering children
18+
useEffect(() => {
19+
if (api?.url && userData?.token) {
20+
fetchPrometheusOidcEnabled(api.url, userData.token).then((enabled) => {
21+
if (enabled) {
22+
setHeaders({ Authorization: `Bearer ${userData.token}` });
23+
}
24+
setOidcEnabled(enabled);
25+
});
26+
}
27+
}, [api?.url, userData?.token]);
28+
29+
if (oidcEnabled === null) {
30+
return <Loader size="massive" centered={true} aria-label="loading" />;
31+
}
32+
33+
return <>{children}</>;
34+
}

ui/src/services/ApiClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class ApiClient {
1212

1313
setHeaders = (headers) => {
1414
// @ts-expect-error - FIXME when you are working on it
15-
this.headers = headers;
15+
this.headers = { ...this.headers, ...headers };
1616
};
1717

1818
async get(endpoint, params = {}, opts = {}) {

ui/src/services/prometheus/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ export function initialize(apiUrl: string) {
5151
prometheusApiClient = new ApiClient({ apiUrl });
5252
}
5353

54+
export function setHeaders(headers: Record<string, string>) {
55+
if (prometheusApiClient) {
56+
prometheusApiClient.setHeaders(headers);
57+
}
58+
}
59+
5460
export function getAlerts() {
5561
if (prometheusApiClient) {
5662
return prometheusApiClient.get('/api/v1/alerts');
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { fetchPrometheusOidcEnabled } from './fetchOidcEnabled';
2+
3+
const mockFetch = jest.fn();
4+
window.fetch = mockFetch;
5+
6+
const mockConfigMapResponse = (enableOidc: boolean) => ({
7+
data: {
8+
'config.yaml': `
9+
apiVersion: addons.metalk8s.scality.com
10+
kind: PrometheusConfig
11+
spec:
12+
config:
13+
enable_oidc_authentication: ${enableOidc}
14+
`,
15+
},
16+
});
17+
18+
describe('fetchPrometheusOidcEnabled', () => {
19+
afterEach(() => {
20+
mockFetch.mockReset();
21+
});
22+
23+
it('returns true when OIDC authentication is enabled', async () => {
24+
mockFetch.mockResolvedValue({
25+
status: 200,
26+
json: () => Promise.resolve(mockConfigMapResponse(true)),
27+
});
28+
29+
const result = await fetchPrometheusOidcEnabled(
30+
'https://k8s-api.test',
31+
'test-token',
32+
);
33+
expect(result).toBe(true);
34+
});
35+
36+
it('returns false when OIDC authentication is disabled', async () => {
37+
mockFetch.mockResolvedValue({
38+
status: 200,
39+
json: () => Promise.resolve(mockConfigMapResponse(false)),
40+
});
41+
42+
const result = await fetchPrometheusOidcEnabled(
43+
'https://k8s-api.test',
44+
'test-token',
45+
);
46+
expect(result).toBe(false);
47+
});
48+
49+
it('returns false when the API returns a non-200 status', async () => {
50+
mockFetch.mockResolvedValue({
51+
status: 500,
52+
json: () => Promise.resolve({}),
53+
});
54+
55+
const result = await fetchPrometheusOidcEnabled(
56+
'https://k8s-api.test',
57+
'test-token',
58+
);
59+
expect(result).toBe(false);
60+
});
61+
62+
it('returns false and logs error when fetch throws', async () => {
63+
const consoleSpy = jest
64+
.spyOn(console, 'error')
65+
.mockImplementation(() => {});
66+
mockFetch.mockRejectedValue(new Error('Network error'));
67+
68+
const result = await fetchPrometheusOidcEnabled(
69+
'https://k8s-api.test',
70+
'test-token',
71+
);
72+
expect(result).toBe(false);
73+
expect(consoleSpy).toHaveBeenCalledWith(
74+
'Failed to fetch Prometheus OIDC config:',
75+
expect.any(Error),
76+
);
77+
consoleSpy.mockRestore();
78+
});
79+
80+
it('returns false when config.yaml is missing', async () => {
81+
mockFetch.mockResolvedValue({
82+
status: 200,
83+
json: () => Promise.resolve({ data: {} }),
84+
});
85+
86+
const result = await fetchPrometheusOidcEnabled(
87+
'https://k8s-api.test',
88+
'test-token',
89+
);
90+
expect(result).toBe(false);
91+
});
92+
93+
it('calls the correct URL with correct headers', async () => {
94+
mockFetch.mockResolvedValue({
95+
status: 200,
96+
json: () => Promise.resolve(mockConfigMapResponse(false)),
97+
});
98+
99+
await fetchPrometheusOidcEnabled('https://k8s-api.test', 'my-token');
100+
101+
expect(mockFetch).toHaveBeenCalledWith(
102+
'https://k8s-api.test/api/v1/namespaces/metalk8s-monitoring/configmaps/metalk8s-prometheus-config',
103+
{
104+
method: 'GET',
105+
headers: {
106+
'Content-Type': 'application/json',
107+
Authorization: 'Bearer my-token',
108+
},
109+
},
110+
);
111+
});
112+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import YAML from 'yaml';
2+
3+
type PrometheusConfig = {
4+
apiVersion: string;
5+
kind: string;
6+
spec: {
7+
config?: {
8+
enable_oidc_authentication?: boolean;
9+
};
10+
};
11+
};
12+
13+
export async function fetchPrometheusOidcEnabled(
14+
k8sApiUrl: string,
15+
token: string,
16+
): Promise<boolean> {
17+
try {
18+
const response = await fetch(
19+
`${k8sApiUrl}/api/v1/namespaces/metalk8s-monitoring/configmaps/metalk8s-prometheus-config`,
20+
{
21+
method: 'GET',
22+
headers: {
23+
'Content-Type': 'application/json',
24+
Authorization: `Bearer ${token}`,
25+
},
26+
},
27+
);
28+
29+
if (response.status !== 200) {
30+
return false;
31+
}
32+
33+
const configMap = await response.json();
34+
const rawConfig = configMap.data?.['config.yaml'] || '';
35+
const config: PrometheusConfig = YAML.parse(rawConfig);
36+
return config.spec?.config?.enable_oidc_authentication === true;
37+
} catch (error) {
38+
console.error('Failed to fetch Prometheus OIDC config:', error);
39+
return false;
40+
}
41+
}

0 commit comments

Comments
 (0)