Alerting: Show a warning when a template has been potentially misconfigured (#94698)

This commit is contained in:
Tom Ratcliffe 2024-10-15 12:00:20 +01:00 committed by GitHub
parent 016dea1143
commit 0841497cad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 159 additions and 138 deletions

View File

@ -6,3 +6,4 @@
import { Text } from '@grafana/ui'; import { Text } from '@grafana/ui';
export const PrimaryText = ({ content }: { content: string }) => <Text color="primary">{content}</Text>; export const PrimaryText = ({ content }: { content: string }) => <Text color="primary">{content}</Text>;
export const CodeText = ({ content }: { content: string }) => <Text variant="code">{content}</Text>;

View File

@ -126,6 +126,13 @@ describe('contact points', () => {
}); });
}); });
describe('templates tab', () => {
it('shows a warning when a template is misconfigured', async () => {
renderWithProvider(<ContactPointsPageContents />, { initialEntries: ['/?tab=templates'] });
expect((await screen.findAllByText(/^misconfigured$/i))[0]).toBeInTheDocument();
});
});
it('should show / hide loading states, have all actions enabled', async () => { it('should show / hide loading states, have all actions enabled', async () => {
renderWithProvider(<ContactPointsPageContents />); renderWithProvider(<ContactPointsPageContents />);

View File

@ -3,10 +3,13 @@
"slack-template": "{{ define \"slack-template\" }} Custom slack template {{ end }}", "slack-template": "{{ define \"slack-template\" }} Custom slack template {{ end }}",
"custom-email": "{{ define \"custom-email\" }} Custom email template {{ end }}", "custom-email": "{{ define \"custom-email\" }} Custom email template {{ end }}",
"provisioned-template": "{{ define \"provisioned-template\" }} Custom provisioned template {{ end }}", "provisioned-template": "{{ define \"provisioned-template\" }} Custom provisioned template {{ end }}",
"template with spaces": "{{ define \"template with spaces\" }} Custom template with spaces in the name {{ end }}" "template with spaces": "{{ define \"template with spaces\" }} Custom template with spaces in the name {{ end }}",
"misconfigured-template": "{{ define \"misconfigured template\" }} Template that is defined in template_files but not templates {{ end }}",
"misconfigured and provisioned": "{{ define \"misconfigured and provisioned template\" }} Provisioned template that is defined in template_files but not templates {{ end }}"
}, },
"template_file_provenances": { "template_file_provenances": {
"provisioned-template": "api" "provisioned-template": "api",
"misconfigured and provisioned": "api"
}, },
"alertmanager_config": { "alertmanager_config": {
"route": { "route": {

View File

@ -26,6 +26,7 @@ export interface NotificationTemplate {
title: string; title: string;
content: string; content: string;
provenance: string; provenance: string;
missing?: boolean;
} }
const { useGetAlertmanagerConfigurationQuery, useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; const { useGetAlertmanagerConfigurationQuery, useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi;
@ -84,12 +85,15 @@ function templateGroupToTemplate(
} }
function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemplate[] { function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemplate[] {
const { alertmanager_config } = config;
const { templates = [] } = alertmanager_config;
return Object.entries(config.template_files).map(([title, content]) => ({ return Object.entries(config.template_files).map(([title, content]) => ({
uid: title, uid: title,
title, title,
content, content,
// Undefined, null or empty string should be converted to PROVENANCE_NONE // Undefined, null or empty string should be converted to PROVENANCE_NONE
provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE, provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE,
missing: !templates.includes(title),
})); }));
} }

View File

@ -97,7 +97,11 @@ describe('alerting API server disabled', () => {
); );
const testBody = await testRequest?.json(); const testBody = await testRequest?.json();
const saveBody = await saveRequest?.json(); const fullSaveBody = await saveRequest?.json();
// Only snapshot and check the receivers, as we don't want other tests to break this
// just because we added something new to the mock config
const saveBody = fullSaveBody.alertmanager_config.receivers;
expect([testBody]).toMatchSnapshot(); expect([testBody]).toMatchSnapshot();
expect([saveBody]).toMatchSnapshot(); expect([saveBody]).toMatchSnapshot();

View File

@ -1,8 +1,10 @@
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { logError } from '@grafana/runtime'; import { logError } from '@grafana/runtime';
import { ConfirmModal, useStyles2 } from '@grafana/ui'; import { Badge, ConfirmModal, Tooltip, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { t, Trans } from 'app/core/internationalization';
import { CodeText } from 'app/features/alerting/unified/components/common/TextVariants';
import { Authorize } from '../../components/Authorize'; import { Authorize } from '../../components/Authorize';
import { AlertmanagerAction } from '../../hooks/useAbilities'; import { AlertmanagerAction } from '../../hooks/useAbilities';
@ -116,8 +118,8 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const { isProvisioned } = useNotificationTemplateMetadata(notificationTemplate); const { isProvisioned } = useNotificationTemplateMetadata(notificationTemplate);
const { uid, title: name, content: template } = notificationTemplate; const { uid, title: name, content: template, missing } = notificationTemplate;
const misconfiguredBadgeText = t('alerting.templates.misconfigured-badge-text', 'Misconfigured');
return ( return (
<Fragment key={uid}> <Fragment key={uid}>
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}> <tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
@ -125,7 +127,25 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
<CollapseToggle isCollapsed={!isExpanded} onToggle={() => setIsExpanded(!isExpanded)} /> <CollapseToggle isCollapsed={!isExpanded} onToggle={() => setIsExpanded(!isExpanded)} />
</td> </td>
<td> <td>
{name} {isProvisioned && <ProvisioningBadge />} {name} {isProvisioned && <ProvisioningBadge />}{' '}
{missing && (
<Tooltip
content={
<>
<Trans i18nKey="alerting.templates.misconfigured-warning">This template is misconfigured.</Trans>
<br />
<Trans i18nKey="alerting.templates.misconfigured-warning-details">
Templates must be defined in both the <CodeText content="template_files" /> and{' '}
<CodeText content="templates" /> sections of your alertmanager configuration.
</Trans>
</>
}
>
<span>
<Badge text={misconfiguredBadgeText} color="orange" />
</span>
</Tooltip>
)}
</td> </td>
<td className={tableStyles.actionsCell}> <td className={tableStyles.actionsCell}>
{isProvisioned && ( {isProvisioned && (

View File

@ -34,142 +34,113 @@ exports[`alerting API server disabled should be able to test and save a receiver
exports[`alerting API server disabled should be able to test and save a receiver 2`] = ` exports[`alerting API server disabled should be able to test and save a receiver 2`] = `
[ [
{ [
"alertmanager_config": { {
"mute_time_intervals": [], "grafana_managed_receiver_configs": [
"receivers": [
{ {
"grafana_managed_receiver_configs": [ "disableResolveMessage": false,
{
"disableResolveMessage": false,
"name": "grafana-default-email",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "xeKQrBrnk",
},
],
"name": "grafana-default-email", "name": "grafana-default-email",
}, "secureFields": {},
{ "settings": {
"grafana_managed_receiver_configs": [ "addresses": "gilles.demey@grafana.com",
{ "singleEmail": false,
"disableResolveMessage": false,
"name": "provisioned-contact-point",
"provenance": "api",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "s8SdCVjnk",
},
],
"name": "provisioned-contact-point",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "lotsa-emails",
"secureFields": {},
"settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
},
],
"name": "lotsa-emails",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts",
},
"type": "slack",
"uid": "c02ad56a-31da-46b9-becb-4348ec0890fd",
},
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts2",
},
"type": "slack",
"uid": "b286a3be-f690-49e2-8605-b075cbace2df",
},
],
"name": "Slack with multiple channels",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Oncall-integration",
"settings": {
"url": "https://oncall-endpoint.example.com",
},
"type": "oncall",
},
],
"name": "OnCall Conctact point",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "my new receiver",
"secureSettings": {},
"settings": {
"addresses": "tester@grafana.com",
"singleEmail": false,
},
"type": "email",
},
],
"name": "my new receiver",
},
],
"route": {
"receiver": "grafana-default-email",
"routes": [
{
"receiver": "provisioned-contact-point",
}, },
], "type": "email",
}, "uid": "xeKQrBrnk",
"templates": [ },
"slack-template",
"custom-email",
"provisioned-template",
"template with spaces",
], ],
"time_intervals": [], "name": "grafana-default-email",
}, },
"template_file_provenances": { {
"provisioned-template": "api", "grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "provisioned-contact-point",
"provenance": "api",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "s8SdCVjnk",
},
],
"name": "provisioned-contact-point",
}, },
"template_files": { {
"custom-email": "{{ define "custom-email" }} Custom email template {{ end }}", "grafana_managed_receiver_configs": [
"provisioned-template": "{{ define "provisioned-template" }} Custom provisioned template {{ end }}", {
"slack-template": "{{ define "slack-template" }} Custom slack template {{ end }}", "disableResolveMessage": false,
"template with spaces": "{{ define "template with spaces" }} Custom template with spaces in the name {{ end }}", "name": "lotsa-emails",
"secureFields": {},
"settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
},
],
"name": "lotsa-emails",
}, },
}, {
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts",
},
"type": "slack",
"uid": "c02ad56a-31da-46b9-becb-4348ec0890fd",
},
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts2",
},
"type": "slack",
"uid": "b286a3be-f690-49e2-8605-b075cbace2df",
},
],
"name": "Slack with multiple channels",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Oncall-integration",
"settings": {
"url": "https://oncall-endpoint.example.com",
},
"type": "oncall",
},
],
"name": "OnCall Conctact point",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "my new receiver",
"secureSettings": {},
"settings": {
"addresses": "tester@grafana.com",
"singleEmail": false,
},
"type": "email",
},
],
"name": "my new receiver",
},
],
] ]
`; `;

View File

@ -2,6 +2,7 @@ import { ReactNode } from 'react';
import { render, screen, userEvent } from 'test/test-utils'; import { render, screen, userEvent } from 'test/test-utils';
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types'; import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
import alertmanagerConfigMock from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
@ -98,7 +99,7 @@ describe('TemplatesPicker', () => {
const input = screen.getByRole('combobox'); const input = screen.getByRole('combobox');
expect(screen.queryByText('slack-template')).not.toBeInTheDocument(); expect(screen.queryByText('slack-template')).not.toBeInTheDocument();
await userEvent.click(input); await userEvent.click(input);
expect(screen.getAllByRole('option')).toHaveLength(7); // 4 templates in mock plus 3 in the default template expect(screen.getAllByRole('option')).toHaveLength(Object.keys(alertmanagerConfigMock.template_files).length + 3); // 4 templates in mock plus 3 in the default template
const template = screen.getByRole('option', { name: 'slack-template' }); const template = screen.getByRole('option', { name: 'slack-template' });
await userEvent.click(template); await userEvent.click(template);
expect(screen.getByText('slack-template')).toBeInTheDocument(); expect(screen.getByText('slack-template')).toBeInTheDocument();

View File

@ -284,6 +284,11 @@
"ofQuery": { "ofQuery": {
"To": "TO" "To": "TO"
} }
},
"templates": {
"misconfigured-badge-text": "Misconfigured",
"misconfigured-warning": "This template is misconfigured.",
"misconfigured-warning-details": "Templates must be defined in both the <1></1> and <4></4> sections of your alertmanager configuration."
} }
}, },
"annotations": { "annotations": {

View File

@ -284,6 +284,11 @@
"ofQuery": { "ofQuery": {
"To": "ŦØ" "To": "ŦØ"
} }
},
"templates": {
"misconfigured-badge-text": "Mįşčőʼnƒįģūřęđ",
"misconfigured-warning": "Ŧĥįş ŧęmpľäŧę įş mįşčőʼnƒįģūřęđ.",
"misconfigured-warning-details": "Ŧęmpľäŧęş mūşŧ þę đęƒįʼnęđ įʼn þőŧĥ ŧĥę <1></1> äʼnđ <4></4> şęčŧįőʼnş őƒ yőūř äľęřŧmäʼnäģęř čőʼnƒįģūřäŧįőʼn."
} }
}, },
"annotations": { "annotations": {