1import { urlUtil } from '@grafana/data';
2import { getBackendSrv } from '@grafana/runtime';
3import {
4  AlertmanagerAlert,
5  AlertManagerCortexConfig,
6  AlertmanagerGroup,
7  AlertmanagerStatus,
8  ExternalAlertmanagersResponse,
9  Matcher,
10  Receiver,
11  Silence,
12  SilenceCreatePayload,
13  TestReceiversAlert,
14  TestReceiversPayload,
15  TestReceiversResult,
16} from 'app/plugins/datasource/alertmanager/types';
17import { lastValueFrom } from 'rxjs';
18import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
19import { isFetchError } from '../utils/alertmanager';
20
21// "grafana" for grafana-managed, otherwise a datasource name
22export async function fetchAlertManagerConfig(alertManagerSourceName: string): Promise<AlertManagerCortexConfig> {
23  try {
24    const result = await lastValueFrom(
25      getBackendSrv().fetch<AlertManagerCortexConfig>({
26        url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/alerts`,
27        showErrorAlert: false,
28        showSuccessAlert: false,
29      })
30    );
31    return {
32      template_files: result.data.template_files ?? {},
33      alertmanager_config: result.data.alertmanager_config ?? {},
34    };
35  } catch (e) {
36    // if no config has been uploaded to grafana, it returns error instead of latest config
37    if (
38      alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
39      e.data?.message?.includes('could not find an Alertmanager configuration')
40    ) {
41      return {
42        template_files: {},
43        alertmanager_config: {},
44      };
45    }
46    throw e;
47  }
48}
49
50export async function updateAlertManagerConfig(
51  alertManagerSourceName: string,
52  config: AlertManagerCortexConfig
53): Promise<void> {
54  await lastValueFrom(
55    getBackendSrv().fetch({
56      method: 'POST',
57      url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/alerts`,
58      data: config,
59      showErrorAlert: false,
60      showSuccessAlert: false,
61    })
62  );
63}
64
65export async function deleteAlertManagerConfig(alertManagerSourceName: string): Promise<void> {
66  await lastValueFrom(
67    getBackendSrv().fetch({
68      method: 'DELETE',
69      url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/alerts`,
70      showErrorAlert: false,
71      showSuccessAlert: false,
72    })
73  );
74}
75
76export async function fetchSilences(alertManagerSourceName: string): Promise<Silence[]> {
77  const result = await lastValueFrom(
78    getBackendSrv().fetch<Silence[]>({
79      url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/api/v2/silences`,
80      showErrorAlert: false,
81      showSuccessAlert: false,
82    })
83  );
84  return result.data;
85}
86
87// returns the new silence ID. Even in the case of an update, a new silence is created and the previous one expired.
88export async function createOrUpdateSilence(
89  alertmanagerSourceName: string,
90  payload: SilenceCreatePayload
91): Promise<Silence> {
92  const result = await lastValueFrom(
93    getBackendSrv().fetch<Silence>({
94      url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences`,
95      data: payload,
96      showErrorAlert: false,
97      showSuccessAlert: false,
98      method: 'POST',
99    })
100  );
101  return result.data;
102}
103
104export async function expireSilence(alertmanagerSourceName: string, silenceID: string): Promise<void> {
105  await getBackendSrv().delete(
106    `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silence/${encodeURIComponent(silenceID)}`
107  );
108}
109
110export async function fetchAlerts(
111  alertmanagerSourceName: string,
112  matchers?: Matcher[],
113  silenced = true,
114  active = true,
115  inhibited = true
116): Promise<AlertmanagerAlert[]> {
117  const filters =
118    urlUtil.toUrlParams({ silenced, active, inhibited }) +
119      matchers
120        ?.map(
121          (matcher) =>
122            `filter=${encodeURIComponent(
123              `${escapeQuotes(matcher.name)}=${matcher.isRegex ? '~' : ''}"${escapeQuotes(matcher.value)}"`
124            )}`
125        )
126        .join('&') || '';
127
128  const result = await lastValueFrom(
129    getBackendSrv().fetch<AlertmanagerAlert[]>({
130      url:
131        `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/alerts` +
132        (filters ? '?' + filters : ''),
133      showErrorAlert: false,
134      showSuccessAlert: false,
135    })
136  );
137
138  return result.data;
139}
140
141export async function fetchAlertGroups(alertmanagerSourceName: string): Promise<AlertmanagerGroup[]> {
142  const result = await lastValueFrom(
143    getBackendSrv().fetch<AlertmanagerGroup[]>({
144      url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/alerts/groups`,
145      showErrorAlert: false,
146      showSuccessAlert: false,
147    })
148  );
149
150  return result.data;
151}
152
153export async function fetchStatus(alertManagerSourceName: string): Promise<AlertmanagerStatus> {
154  const result = await lastValueFrom(
155    getBackendSrv().fetch<AlertmanagerStatus>({
156      url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/api/v2/status`,
157      showErrorAlert: false,
158      showSuccessAlert: false,
159    })
160  );
161
162  return result.data;
163}
164
165export async function testReceivers(
166  alertManagerSourceName: string,
167  receivers: Receiver[],
168  alert?: TestReceiversAlert
169): Promise<void> {
170  const data: TestReceiversPayload = {
171    receivers,
172    alert,
173  };
174  try {
175    const result = await lastValueFrom(
176      getBackendSrv().fetch<TestReceiversResult>({
177        method: 'POST',
178        data,
179        url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/receivers/test`,
180        showErrorAlert: false,
181        showSuccessAlert: false,
182      })
183    );
184
185    if (receiversResponseContainsErrors(result.data)) {
186      throw new Error(getReceiverResultError(result.data));
187    }
188  } catch (error) {
189    if (isFetchError(error) && isTestReceiversResult(error.data) && receiversResponseContainsErrors(error.data)) {
190      throw new Error(getReceiverResultError(error.data));
191    }
192
193    throw error;
194  }
195}
196
197function receiversResponseContainsErrors(result: TestReceiversResult) {
198  return result.receivers.some((receiver) =>
199    receiver.grafana_managed_receiver_configs.some((config) => config.status === 'failed')
200  );
201}
202
203function isTestReceiversResult(data: any): data is TestReceiversResult {
204  const receivers = data?.receivers;
205
206  if (Array.isArray(receivers)) {
207    return receivers.every(
208      (receiver: any) => typeof receiver.name === 'string' && Array.isArray(receiver.grafana_managed_receiver_configs)
209    );
210  }
211
212  return false;
213}
214
215function getReceiverResultError(receiversResult: TestReceiversResult) {
216  return receiversResult.receivers
217    .flatMap((receiver) =>
218      receiver.grafana_managed_receiver_configs
219        .filter((receiver) => receiver.status === 'failed')
220        .map((receiver) => receiver.error ?? 'Unknown error.')
221    )
222    .join('; ');
223}
224
225export async function addAlertManagers(alertManagers: string[]): Promise<void> {
226  await lastValueFrom(
227    getBackendSrv().fetch({
228      method: 'POST',
229      data: { alertmanagers: alertManagers },
230      url: '/api/v1/ngalert/admin_config',
231      showErrorAlert: false,
232      showSuccessAlert: false,
233    })
234  ).then(() => {
235    fetchExternalAlertmanagerConfig();
236  });
237}
238
239export async function fetchExternalAlertmanagers(): Promise<ExternalAlertmanagersResponse> {
240  const result = await lastValueFrom(
241    getBackendSrv().fetch<ExternalAlertmanagersResponse>({
242      method: 'GET',
243      url: '/api/v1/ngalert/alertmanagers',
244    })
245  );
246
247  return result.data;
248}
249
250export async function fetchExternalAlertmanagerConfig(): Promise<{ alertmanagers: string[] }> {
251  const result = await lastValueFrom(
252    getBackendSrv().fetch<{ alertmanagers: string[] }>({
253      method: 'GET',
254      url: '/api/v1/ngalert/admin_config',
255      showErrorAlert: false,
256    })
257  );
258
259  return result.data;
260}
261
262function escapeQuotes(value: string): string {
263  return value.replace(/"/g, '\\"');
264}
265