/
githubmirror
/
grafana
Обзор
Документация
Войти
/
githubmirror
/
grafana
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
pkg/services/ngalert/api/api_configuration.go
273 строки
11 KB
Tito Lins
Alerting: auto-sync Mimir Alertmanager config into Grafana ExtraConfiguration (#119910)
12 май 2026, 11:17
Не верифицирован
12 май 2026, 11:17
5c06239
Код
Авторство
О чём код?
package api import ( "context" "errors" "fmt" "net/http" "slices" "strings" v1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/open-feature/go-sdk/openfeature" ) // syncableAMImplementations lists the alertmanager datasource implementations // whose configuration the external AM sync worker can fetch. var syncableAMImplementations = []string{"mimir", "cortex"} type ConfigSrv struct { datasourceService datasources.DataSourceService alertmanagerProvider ExternalAlertmanagerProvider store store.AdminConfigurationStore cfg *setting.UnifiedAlertingSettings log log.Logger } func (srv ConfigSrv) RouteGetAlertmanagers(c *contextmodel.ReqContext) response.Response { urls := srv.alertmanagerProvider.AlertmanagersFor(c.GetOrgID()) droppedURLs := srv.alertmanagerProvider.DroppedAlertmanagersFor(c.GetOrgID()) ams := v1.AlertManagersResult{Active: make([]v1.AlertManager, len(urls)), Dropped: make([]v1.AlertManager, len(droppedURLs))} for i, url := range urls { ams.Active[i].URL = url.String() } for i, url := range droppedURLs { ams.Dropped[i].URL = url.String() } return response.JSON(http.StatusOK, apimodels.GettableAlertmanagers{ Status: "success", Data: ams, }) } func (srv ConfigSrv) RouteGetNGalertConfig(c *contextmodel.ReqContext) response.Response { if c.GetOrgRole() != org.RoleAdmin { return accessForbiddenResp() } // Operator-level ini value, if set, is the effective UID regardless of // what's stored in the database (matches the sync worker's resolution // order in resolveExternalAMUID). Surface it on read so the API reflects // what the system will actually use. iniUID := "" if srv.cfg != nil { iniUID = srv.cfg.ExternalAlertmanagerUID } cfg, err := srv.store.GetAdminConfiguration(c.GetOrgID()) if err != nil && !errors.Is(err, store.ErrNoAdminConfiguration) { msg := "failed to fetch admin configuration from the database" srv.log.Error(msg, "error", err) return ErrResp(http.StatusInternalServerError, err, msg) } if cfg == nil { if iniUID != "" { return response.JSON(http.StatusOK, apimodels.GettableNGalertConfig{ ExternalAlertmanagerUID: iniUID, }) } return ErrResp(http.StatusNotFound, store.ErrNoAdminConfiguration, "") } var resp apimodels.GettableNGalertConfig if cfg.SendAlertsTo != nil { resp.AlertmanagersChoice = apimodels.AlertmanagersChoice(cfg.SendAlertsTo.String()) } if iniUID != "" { resp.ExternalAlertmanagerUID = iniUID } else if cfg.ExternalAlertmanagerUID != nil { resp.ExternalAlertmanagerUID = *cfg.ExternalAlertmanagerUID } return response.JSON(http.StatusOK, resp) } func (srv ConfigSrv) RoutePostNGalertConfig(c *contextmodel.ReqContext, body apimodels.PostableNGalertConfig) response.Response { ctx := c.Req.Context() ofClient := openfeature.NewDefaultClient() if c.GetOrgRole() != org.RoleAdmin { return accessForbiddenResp() } if body.AlertmanagersChoice == nil && body.ExternalAlertmanagerUID == nil { return response.Error(http.StatusBadRequest, "No fields to update", nil) } adminConfig := ngmodels.AdminConfiguration{ OrgID: c.GetOrgID(), } if body.AlertmanagersChoice != nil { sendAlertsTo, err := ngmodels.StringToAlertmanagersChoice(string(*body.AlertmanagersChoice)) if err != nil { return response.Error(http.StatusBadRequest, "Invalid alertmanager choice specified", err) } disableExternal := ofClient.Boolean(ctx, featuremgmt.FlagAlertingDisableSendAlertsExternal, false, openfeature.TransactionContext(ctx)) if disableExternal && sendAlertsTo != ngmodels.InternalAlertmanager { return response.Error(http.StatusBadRequest, "Sending alerts to external alertmanagers is disallowed on this instance", nil) } externalAlertmanagers, err := srv.externalAlertmanagers(ctx, c.GetOrgID()) if err != nil { return response.Error(http.StatusInternalServerError, "Couldn't fetch the external Alertmanagers from datasources", err) } if sendAlertsTo == ngmodels.ExternalAlertmanagers && len(externalAlertmanagers) < 1 { return response.Error(http.StatusBadRequest, "At least one Alertmanager must be provided or configured as a datasource that handles alerts to choose this option", nil) } adminConfig.SendAlertsTo = &sendAlertsTo } if body.ExternalAlertmanagerUID != nil { // When the operator-level ini value is set it is authoritative for all orgs, // so the API must not let users overwrite or clear it via admin_config writes. // Reject any UID write attempt up front (regardless of whether the body value // matches the ini) — the request is meaningless because the ini wins on read // and the sync worker uses the ini value. Checked before the feature-flag // gate so the more authoritative reason wins when both apply. if srv.cfg != nil && srv.cfg.ExternalAlertmanagerUID != "" { return response.Error(http.StatusConflict, "external alertmanager UID is managed by the operator (ini); cannot be changed via API", nil) } // Reject up front when sync is disabled rather than silently dropping the // field. Returning 201 with the value not persisted hides integration bugs // (callers think the UID was saved, but it wasn't). // // We deliberately reject empty-string clears too: if the org had a UID // configured before the flag was disabled, the convert API will keep // returning 409 (because IsExternalAMSyncConfiguredForOrg gates on // configuration, not flag state) until the operator re-enables the flag // and clears the UID, or removes the admin_config row entirely. Recovery // is deliberately operator-driven — the user-facing API stays strict to // surface the inconsistency rather than silently allowing a partial // change with the flag off. if !ofClient.Boolean(ctx, featuremgmt.FlagAlertingSyncExternalAlertmanager, false, openfeature.TransactionContext(ctx)) { return response.Error(http.StatusBadRequest, "external alertmanager UID sync is disabled on this instance", nil) } // Validate the datasource only when the value actually changes, so unrelated // updates (e.g. AlertmanagersChoice only) don't fail because the previously // stored UID is no longer valid. current := "" currentCfg, err := srv.store.GetAdminConfiguration(c.GetOrgID()) if err != nil && !errors.Is(err, store.ErrNoAdminConfiguration) { return response.Error(http.StatusInternalServerError, "failed to fetch admin configuration", err) } if currentCfg != nil && currentCfg.ExternalAlertmanagerUID != nil { current = *currentCfg.ExternalAlertmanagerUID } if *body.ExternalAlertmanagerUID != current && *body.ExternalAlertmanagerUID != "" { ds, err := srv.datasourceService.GetDataSource(ctx, &datasources.GetDataSourceQuery{ UID: *body.ExternalAlertmanagerUID, OrgID: c.GetOrgID(), }) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { return response.Error(http.StatusBadRequest, "datasource not found", err) } return response.Error(http.StatusInternalServerError, "failed to look up datasource", err) } if ds.Type != datasources.DS_ALERTMANAGER { return response.Error(http.StatusBadRequest, "datasource must be of type alertmanager", nil) } impl := strings.ToLower(ds.JsonData.Get("implementation").MustString("")) if !slices.Contains(syncableAMImplementations, impl) { return response.Error(http.StatusBadRequest, fmt.Sprintf( "%q implementation is not supported for sync (must be one of: %s). Use the convert API for manual config import.", impl, strings.Join(syncableAMImplementations, ", ")), nil) } } adminConfig.ExternalAlertmanagerUID = body.ExternalAlertmanagerUID } if err := srv.store.UpdateAdminConfiguration(store.UpdateAdminConfigurationCmd{ AdminConfiguration: &adminConfig, }); err != nil { msg := "failed to save the admin configuration to the database" srv.log.Error(msg, "error", err) return ErrResp(http.StatusInternalServerError, err, msg) } return response.JSON(http.StatusCreated, util.DynMap{"message": "admin configuration updated"}) } func (srv ConfigSrv) RouteDeleteNGalertConfig(c *contextmodel.ReqContext) response.Response { if c.GetOrgRole() != org.RoleAdmin { return accessForbiddenResp() } err := srv.store.DeleteAdminConfiguration(c.GetOrgID()) if err != nil { srv.log.Error("Unable to delete configuration", "error", err) return ErrResp(http.StatusInternalServerError, err, "") } return response.JSON(http.StatusOK, util.DynMap{"message": "admin configuration deleted"}) } // externalAlertmanagers returns the URL of any external alertmanager that is // configured as datasource. The URL does not contain any auth. func (srv ConfigSrv) externalAlertmanagers(ctx context.Context, orgID int64) ([]string, error) { var alertmanagers []string query := &datasources.GetDataSourcesByTypeQuery{ OrgID: orgID, Type: datasources.DS_ALERTMANAGER, } dataSources, err := srv.datasourceService.GetDataSourcesByType(ctx, query) if err != nil { return nil, fmt.Errorf("failed to fetch datasources for org: %w", err) } for _, ds := range dataSources { if ds.JsonData.Get(apimodels.HandleGrafanaManagedAlerts).MustBool(false) { // we don't need to build the exact URL as we only need // to know if any is set alertmanagers = append(alertmanagers, ds.UID) } } return alertmanagers, nil } func (srv ConfigSrv) RouteGetAlertingStatus(c *contextmodel.ReqContext) response.Response { sendsAlertsTo := ngmodels.InternalAlertmanager cfg, err := srv.store.GetAdminConfiguration(c.GetOrgID()) if err != nil && !errors.Is(err, store.ErrNoAdminConfiguration) { msg := "failed to fetch configuration from the database" srv.log.Error(msg, "error", err) return ErrResp(http.StatusInternalServerError, err, msg) } if cfg != nil && cfg.SendAlertsTo != nil { sendsAlertsTo = *cfg.SendAlertsTo } // handle errors externalAlertManagers, err := srv.externalAlertmanagers(c.Req.Context(), c.GetOrgID()) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } resp := apimodels.AlertingStatus{ AlertmanagersChoice: apimodels.AlertmanagersChoice(sendsAlertsTo.String()), NumExternalAlertmanagers: len(externalAlertManagers), } return response.JSON(http.StatusOK, resp) }