diff --git a/docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md b/docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md index e6a7d5207cc..6b46408c689 100644 --- a/docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md +++ b/docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md @@ -15,3 +15,25 @@ Grafana includes built-in support for Prometheus Alertmanager. By default, notif Grafana 8 alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Cortex or Loki data sources. {{< figure max-width="40%" src="/static/img/docs/alerting/unified/contact-points-select-am-8-0.gif" max-width="250px" caption="Select Alertmanager" >}} + +You can configure one or several external Alertmanagers to receive alerts from Grafana. Once configured, both the embedded Alertmanager **and** any configured external Alertmanagers will receive _all_ alerts. + +You can do the setup in the "Admin" tab within the Grafana v8 Alerts UI. + +### Add a new external Alertmanager + +1. In the Grafana menu, click the Alerting (bell) icon to open the Alerting page listing existing alerts. +2. Click **Admin** and then scroll down to the External Alertmanager section. +3. Click **Add Alertmanager** and a modal opens. +4. Add the URL and the port for the external Alertmanager. You do not need to specify the path suffix, for example, `/api/v(1|2)/alerts`. Grafana automatically adds this. + +The external URL is listed in the table with a pending status. Once Grafana verifies that the Alertmanager is discovered, the status changes to active. No requests are made to the external Alertmanager at this point; the verification signals that alerts are ready to be sent. + +### Edit an external Alertmanager + +1. Click the pen symbol to the right of the Alertmanager row in the table. +2. When the edit modal opens, you can view all the URLs that were added. + +The edited URL will be pending until Grafana verifies it again. + +{{< figure max-width="40%" src="/static/img/docs/alerting/unified/ext-alertmanager-active.png" max-width="650px" caption="External Alertmanagers" >}} diff --git a/pkg/services/ngalert/api/api_admin.go b/pkg/services/ngalert/api/api_admin.go index e9113f54388..42c94562258 100644 --- a/pkg/services/ngalert/api/api_admin.go +++ b/pkg/services/ngalert/api/api_admin.go @@ -70,6 +70,12 @@ func (srv AdminSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels. OrgID: c.OrgId, } + if err := cfg.Validate(); err != nil { + msg := "failed to validate admin configuration" + srv.log.Error(msg, "err", err) + return ErrResp(http.StatusBadRequest, err, msg) + } + cmd := store.UpdateAdminConfigurationCmd{AdminConfiguration: cfg} if err := srv.store.UpdateAdminConfiguration(cmd); err != nil { msg := "failed to save the admin configuration to the database" diff --git a/pkg/services/ngalert/models/admin_configuration.go b/pkg/services/ngalert/models/admin_configuration.go index a3323718150..80ae11d40a0 100644 --- a/pkg/services/ngalert/models/admin_configuration.go +++ b/pkg/services/ngalert/models/admin_configuration.go @@ -3,6 +3,7 @@ package models import ( "crypto/sha256" "fmt" + "net/url" ) // AdminConfiguration represents the ngalert administration configuration settings. @@ -22,3 +23,14 @@ func (ac *AdminConfiguration) AsSHA256() string { _, _ = h.Write([]byte(fmt.Sprintf("%v", ac.Alertmanagers))) return fmt.Sprintf("%x", h.Sum(nil)) } + +func (ac *AdminConfiguration) Validate() error { + for _, u := range ac.Alertmanagers { + _, err := url.Parse(u) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/services/ngalert/models/admin_configuration_test.go b/pkg/services/ngalert/models/admin_configuration_test.go new file mode 100644 index 00000000000..ff932652cfc --- /dev/null +++ b/pkg/services/ngalert/models/admin_configuration_test.go @@ -0,0 +1,58 @@ +package models + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAdminConfiguration_AsSHA256(t *testing.T) { + tc := []struct { + name string + ac *AdminConfiguration + ciphertext string + }{ + { + name: "AsSHA256", + ac: &AdminConfiguration{Alertmanagers: []string{"http://localhost:9093"}}, + ciphertext: "3ec9db375a5ba12f7c7b704922cf4b8e21a31e30d85be2386803829f0ee24410", + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.ciphertext, tt.ac.AsSHA256()) + }) + } +} + +func TestAdminConfiguration_Validate(t *testing.T) { + tc := []struct { + name string + ac *AdminConfiguration + err error + }{ + { + name: "should return the first error if any of the Alertmanagers URL is invalid", + ac: &AdminConfiguration{Alertmanagers: []string{"http://localhost:9093", "http://›∂-)Æÿ ñ"}}, + err: fmt.Errorf("parse \"http://›∂-)Æÿ ñ\": invalid character \" \" in host name"), + }, + { + name: "should not return any errors if all URLs are valid", + ac: &AdminConfiguration{Alertmanagers: []string{"http://localhost:9093"}}, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + err := tt.ac.Validate() + if tt.err != nil { + require.EqualError(t, err, tt.err.Error()) + return + } + + require.NoError(t, err) + }) + } +} diff --git a/public/app/features/alerting/unified/Admin.tsx b/public/app/features/alerting/unified/Admin.tsx index 509674fc167..21a534cdca6 100644 --- a/public/app/features/alerting/unified/Admin.tsx +++ b/public/app/features/alerting/unified/Admin.tsx @@ -1,150 +1,13 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { Alert, Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form } from '@grafana/ui'; -import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; +import React from 'react'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; -import { AlertManagerPicker } from './components/AlertManagerPicker'; -import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from './utils/datasource'; -import { useDispatch } from 'react-redux'; -import { - deleteAlertManagerConfigAction, - fetchAlertManagerConfigAction, - updateAlertManagerConfigAction, -} from './state/actions'; -import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; -import { initialAsyncRequestState } from './utils/redux'; - -interface FormValues { - configJSON: string; -} +import AlertmanagerConfig from './components/admin/AlertmanagerConfig'; +import { ExternalAlertmanagers } from './components/admin/ExternalAlertmanagers'; export default function Admin(): JSX.Element { - const dispatch = useDispatch(); - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); - const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false); - const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig); - const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig); - const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : false; - - const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs); - - const { result: config, loading: isLoadingConfig, error: loadingError } = - (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState; - - useEffect(() => { - if (alertManagerSourceName) { - dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); - } - }, [alertManagerSourceName, dispatch]); - - const resetConfig = () => { - if (alertManagerSourceName) { - dispatch(deleteAlertManagerConfigAction(alertManagerSourceName)); - } - setShowConfirmDeleteAMConfig(false); - }; - - const defaultValues = useMemo( - (): FormValues => ({ - configJSON: config ? JSON.stringify(config, null, 2) : '', - }), - [config] - ); - - const loading = isDeleting || isLoadingConfig || isSaving; - - const onSubmit = (values: FormValues) => { - if (alertManagerSourceName) { - dispatch( - updateAlertManagerConfigAction({ - newConfig: JSON.parse(values.configJSON), - oldConfig: config, - alertManagerSourceName, - successMessage: 'Alertmanager configuration updated.', - refetch: true, - }) - ); - } - }; - return ( - - {loadingError && !loading && ( - - {loadingError.message || 'Unknown error.'} - - )} - {isDeleting && alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME && ( - - It might take a while... - - )} - {alertManagerSourceName && config && ( -
- {({ register, errors }) => ( - <> - {!readOnly && ( - -