Files
grafana/pkg/services/ngalert/api/api_convert_prometheus.go

811 lines
31 KiB
Go

package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
amconfig "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
prommodel "github.com/prometheus/common/model"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/api/validation"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/prom"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
// datasourceUIDHeader is the name of the header that specifies the UID of the datasource to be used for the rules.
datasourceUIDHeader = "X-Grafana-Alerting-Datasource-UID"
// targetDatasourceUIDHeader is the name of the header that specifies the UID of the target datasource to be used for recording rules.
targetDatasourceUIDHeader = "X-Grafana-Alerting-Target-Datasource-UID"
// If the folderUIDHeader is present, namespaces and rule groups will be created in the specified folder.
// If not, the root folder will be used as the default.
folderUIDHeader = "X-Grafana-Alerting-Folder-UID"
// These headers control the paused state of newly created rules. By default, rules are not paused.
recordingRulesPausedHeader = "X-Grafana-Alerting-Recording-Rules-Paused"
alertRulesPausedHeader = "X-Grafana-Alerting-Alert-Rules-Paused"
// notificationSettingsHeader is the header that specifies the notification settings to be used for the rules.
// The value should be a JSON-encoded AlertRuleNotificationSettings object.
notificationSettingsHeader = "X-Grafana-Alerting-Notification-Settings"
// mergeMatchersHeader is the header that specifies the merge matchers for imported Alertmanager config.
// The value should be comma-separated key=value pairs, e.g., "environment=production,team=alerting".
mergeMatchersHeader = "X-Grafana-Alerting-Merge-Matchers"
// configIdentifierHeader is the header that specifies the identifier for imported Alertmanager config.
configIdentifierHeader = "X-Grafana-Alerting-Config-Identifier"
defaultConfigIdentifier = "default"
)
var (
errDatasourceUIDHeaderMissing = errutil.ValidationFailed(
"alerting.datasourceUIDHeaderMissing",
errutil.WithPublicMessage(fmt.Sprintf("Missing datasource UID header: %s", datasourceUIDHeader)),
).Errorf("missing datasource UID header")
errInvalidHeaderValueMsg = "Invalid value for header {{.Public.Header}}: {{.Public.Error}}"
errInvalidHeaderValueBase = errutil.ValidationFailed("alerting.invalidHeaderValue").MustTemplate(errInvalidHeaderValueMsg, errutil.WithPublic(errInvalidHeaderValueMsg))
errRecordingRulesNotEnabled = errutil.ValidationFailed(
"alerting.recordingRulesNotEnabled",
errutil.WithPublicMessage("Cannot import recording rules: Feature not enabled."),
).Errorf("recording rules not enabled")
)
func errInvalidHeaderValue(header string, err error) error {
return errInvalidHeaderValueBase.Build(errutil.TemplateData{Public: map[string]any{"Header": header, "Error": err}})
}
// ConvertPrometheusSrv converts Prometheus rules to Grafana rules
// and retrieves them in a Prometheus-compatible format.
//
// It is designed to support mimirtool integration, so that rules that work with Mimir
// can be imported into Grafana. It works similarly to the provisioning API,
// where once a rule group is created, it is marked as "provisioned" (via provenance mechanism)
// and is not editable in the UI.
//
// This service returns only rule groups that were initially imported from Prometheus-compatible sources.
// Rule groups not imported from Prometheus are excluded because their original rule definitions are unavailable.
// When a rule group is converted from Prometheus to Grafana, the original definition is preserved alongside
// the Grafana rule and used for reading requests here.
//
// Folder Structure Handling:
// mimirtool does not support nested folder structures, while Grafana allows folder nesting.
// To keep compatibility, this service only returns direct child folders of the working folder
// as namespaces, and rule groups and rules that are directly in these child folders.
//
// For example, given this folder structure in Grafana:
//
// grafana/
// ├── production/
// │ ├── service1/
// │ │ └── alerts/
// │ └── service2/
// └── testing/
// └── service3/
//
// If the working folder is "grafana":
// - Only namespaces "production" and "testing" are returned
// - Only rule groups directly within these folders are included
//
// If the working folder is "production":
// - Only namespaces "service1" and "service2" are returned
// - Only rule groups directly within these folders are included
//
// The "working folder" is specified by the X-Grafana-Alerting-Folder-UID header, which can be set to any folder UID,
// and defaults to the root folder if not provided.
type ConvertPrometheusSrv struct {
cfg *setting.UnifiedAlertingSettings
logger log.Logger
ruleStore RuleStore
datasourceCache datasources.CacheService
alertRuleService *provisioning.AlertRuleService
featureToggles featuremgmt.FeatureToggles
am Alertmanager
}
type Alertmanager interface {
DeleteExtraConfiguration(ctx context.Context, org int64, identifier string) error
SaveAndApplyExtraConfiguration(ctx context.Context, org int64, extraConfig apimodels.ExtraConfiguration) error
GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool) (apimodels.GettableUserConfig, error)
}
func NewConvertPrometheusSrv(
cfg *setting.UnifiedAlertingSettings,
logger log.Logger,
ruleStore RuleStore,
datasourceCache datasources.CacheService,
alertRuleService *provisioning.AlertRuleService,
featureToggles featuremgmt.FeatureToggles,
am Alertmanager,
) *ConvertPrometheusSrv {
return &ConvertPrometheusSrv{
cfg: cfg,
logger: logger,
ruleStore: ruleStore,
datasourceCache: datasourceCache,
alertRuleService: alertRuleService,
featureToggles: featureToggles,
am: am,
}
}
// RouteConvertPrometheusGetRules returns all Grafana-managed alert rules in all namespaces (folders)
// that were imported from a Prometheus-compatible source.
// It responds with a YAML containing a mapping of folders to arrays of Prometheus rule groups.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response {
logger := srv.logger.FromContext(c.Req.Context())
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
folders, err := srv.ruleStore.GetNamespaceChildren(c.Req.Context(), workingFolderUID, c.GetOrgID(), c.SignedInUser)
if len(folders) == 0 || errors.Is(err, dashboards.ErrFolderNotFound) {
// If there is no such folder or no children, return empty response
// because mimirtool expects 200 OK response in this case.
return response.YAML(http.StatusOK, map[string][]apimodels.PrometheusRuleGroup{})
}
if err != nil {
logger.Error("Failed to get folders", "error", err)
return errorToResponse(err)
}
folderUIDs := make([]string, 0, len(folders))
for _, f := range folders {
folderUIDs = append(folderUIDs, f.UID)
}
filterOpts := &provisioning.FilterOptions{
HasPrometheusRuleDefinition: util.Pointer(true),
NamespaceUIDs: folderUIDs,
}
groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
if err != nil {
logger.Error("Failed to get alert groups", "error", err)
return errorToResponse(err)
}
namespaces, err := grafanaNamespacesToPrometheus(groups)
if err != nil {
logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err)
return errorToResponse(err)
}
return response.YAML(http.StatusOK, namespaces)
}
// RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source
// within a specified namespace.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
logger := srv.logger.FromContext(c.Req.Context())
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID)
if err != nil {
return namespaceErrorResponse(err)
}
logger.Info("Deleting all Prometheus-imported rule groups", "folder_uid", namespace.UID, "folder_title", namespaceTitle)
provenance := getProvenance(c)
filterOpts := &provisioning.FilterOptions{
NamespaceUIDs: []string{namespace.UID},
HasPrometheusRuleDefinition: util.Pointer(true),
}
err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, provenance, filterOpts)
if errors.Is(err, models.ErrAlertRuleGroupNotFound) {
return response.Empty(http.StatusNotFound)
}
if err != nil {
logger.Error("Failed to delete rule groups", "folder_uid", namespace.UID, "error", err)
return errorToResponse(err)
}
return successfulResponse()
}
// RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
logger := srv.logger.FromContext(c.Req.Context())
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
folder, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID)
if err != nil {
return namespaceErrorResponse(err)
}
logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group)
provenance := getProvenance(c)
err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, provenance)
if errors.Is(err, models.ErrAlertRuleGroupNotFound) {
return response.Empty(http.StatusNotFound)
}
if err != nil {
logger.Error("Failed to delete rule group", "folder_uid", folder.UID, "group", group, "error", err)
return errorToResponse(err)
}
return successfulResponse()
}
// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
// It responds with a YAML containing a mapping of a single folder to an array of Prometheus rule groups.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
logger := srv.logger.FromContext(c.Req.Context())
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID)
if err != nil {
logger.Error("Failed to get folder", "error", err)
return namespaceErrorResponse(err)
}
filterOpts := &provisioning.FilterOptions{
HasPrometheusRuleDefinition: util.Pointer(true),
NamespaceUIDs: []string{namespace.UID},
}
groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
if err != nil {
logger.Error("Failed to get alert groups", "error", err)
return errorToResponse(err)
}
ns, err := grafanaNamespacesToPrometheus(groups)
if err != nil {
logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err)
return errorToResponse(err)
}
return response.YAML(http.StatusOK, ns)
}
// RouteConvertPrometheusGetRuleGroup retrieves a single rule group for a given namespace (folder)
// in Prometheus-compatible YAML format if it was imported from a Prometheus-compatible source.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
logger := srv.logger.FromContext(c.Req.Context())
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID)
if err != nil {
logger.Error("Failed to get folder", "error", err)
return namespaceErrorResponse(err)
}
if namespace == nil {
return response.Error(http.StatusNotFound, "Folder not found", nil)
}
filterOpts := &provisioning.FilterOptions{
HasPrometheusRuleDefinition: util.Pointer(true),
NamespaceUIDs: []string{namespace.UID},
RuleGroups: []string{group},
}
groupsWithFolders, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
if err != nil {
logger.Error("Failed to get alert group", "error", err)
return errorToResponse(err)
}
if len(groupsWithFolders) == 0 {
return response.Error(http.StatusNotFound, "Rule group not found", nil)
}
if len(groupsWithFolders) > 1 {
logger.Error("Multiple rule groups found when only one was expected", "folder_title", namespaceTitle, "group", group)
// It shouldn't happen, but if we get more than 1 group, we return an error.
return response.Error(http.StatusInternalServerError, "Multiple rule groups found", nil)
}
promGroup, err := grafanaRuleGroupToPrometheus(groupsWithFolders[0].Title, groupsWithFolders[0].Rules)
if err != nil {
logger.Error("Failed to convert Grafana rule to Prometheus format", "error", err)
return errorToResponse(err)
}
return response.YAML(http.StatusOK, promGroup)
}
// RouteConvertPrometheusPostRuleGroup converts a Prometheus rule group into a Grafana rule group
// and creates or updates it within the specified namespace (folder).
//
// If the group already exists and was not imported from a Prometheus-compatible source initially,
// it will not be replaced and an error will be returned.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, promGroup apimodels.PrometheusRuleGroup) response.Response {
return srv.RouteConvertPrometheusPostRuleGroups(c, map[string][]apimodels.PrometheusRuleGroup{namespaceTitle: {promGroup}})
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *contextmodel.ReqContext, promNamespaces map[string][]apimodels.PrometheusRuleGroup) response.Response {
logger := srv.logger.FromContext(c.Req.Context())
// 1. Parse the appropriate headers
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
pauseRecordingRules, err := parseBooleanHeader(c.Req.Header.Get(recordingRulesPausedHeader), recordingRulesPausedHeader)
if err != nil {
return errorToResponse(err)
}
pauseAlertRules, err := parseBooleanHeader(c.Req.Header.Get(alertRulesPausedHeader), alertRulesPausedHeader)
if err != nil {
return errorToResponse(err)
}
datasourceUID := strings.TrimSpace(c.Req.Header.Get(datasourceUIDHeader))
if datasourceUID == "" {
return response.Err(errDatasourceUIDHeaderMissing)
}
ds, err := srv.datasourceCache.GetDatasourceByUID(c.Req.Context(), datasourceUID, c.SignedInUser, c.SkipDSCache)
if err != nil {
logger.Error("Failed to get datasource", "datasource_uid", datasourceUID, "error", err)
return errorToResponse(fmt.Errorf("failed to get datasource: %w", err))
}
// By default the target datasource is the same as the query datasource,
// but if the header "X-Grafana-Alerting-Target-Datasource-UID" is present, we use that instead.
tds := ds
if uid := strings.TrimSpace(c.Req.Header.Get(targetDatasourceUIDHeader)); uid != "" {
tds, err = srv.datasourceCache.GetDatasourceByUID(c.Req.Context(), uid, c.SignedInUser, c.SkipDSCache)
if err != nil {
logger.Error("Failed to get target datasource for recording rules", "datasource_uid", uid, "error", err)
return errorToResponse(fmt.Errorf("failed to get recording rules target datasource: %w", err))
}
}
provenance := getProvenance(c)
// If the provenance is not ConvertedPrometheus, we don't keep the original rule definition.
// This is because the rules can be modified through the UI, which may break compatibility
// with the Prometheus format. We only preserve the original rule definition
// to ensure we can return them in this API in Prometheus format.
keepOriginalRuleDefinition := provenance == models.ProvenanceConvertedPrometheus
notificationSettings, err := parseNotificationSettingsHeader(c)
if err != nil {
logger.Error("Failed to parse notification settings header", "error", err)
return errorToResponse(err)
}
// 2. Convert Prometheus Rules to GMA
grafanaGroups := make([]*models.AlertRuleGroup, 0, len(promNamespaces))
for ns, rgs := range promNamespaces {
logger.Debug("Creating a new namespace", "title", ns)
namespace, errResp := srv.getOrCreateNamespace(c, ns, logger, workingFolderUID)
if errResp != nil {
logger.Error("Failed to create a new namespace", "folder_uid", workingFolderUID)
return errResp
}
for _, rg := range rgs {
// If we're importing recording rules, we can only import them if the feature is enabled,
// and the feature flag that enables configuring target datasources per-rule is also enabled.
if promGroupHasRecordingRules(rg) {
if !srv.cfg.RecordingRules.Enabled {
logger.Error("Cannot import recording rules", "error", errRecordingRulesNotEnabled)
return errorToResponse(errRecordingRulesNotEnabled)
}
}
grafanaGroup, err := srv.convertToGrafanaRuleGroup(
c,
ds,
tds,
namespace.UID,
rg,
pauseRecordingRules,
pauseAlertRules,
keepOriginalRuleDefinition,
notificationSettings,
logger,
)
if err != nil {
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
return errorToResponse(err)
}
grafanaGroups = append(grafanaGroups, grafanaGroup)
}
}
// 3. Update the GMA Rules in the DB
err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance)
if err != nil {
logger.Error("Failed to replace rule groups", "error", err)
return errorToResponse(err)
}
return successfulResponse()
}
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger, workingFolderUID string) (*folder.FolderReference, response.Response) {
logger.Debug("Getting or creating a new folder")
ns, err := srv.ruleStore.GetOrCreateNamespaceByTitle(
c.Req.Context(),
title,
c.GetOrgID(),
c.SignedInUser,
workingFolderUID,
)
if err != nil {
logger.Error("Failed to get or create a new folder", "error", err)
return nil, namespaceErrorResponse(err)
}
logger.Debug("Using folder for the converted rules", "folder_uid", ns.UID)
return ns, nil
}
func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
c *contextmodel.ReqContext,
ds *datasources.DataSource,
tds *datasources.DataSource,
namespaceUID string,
promGroup apimodels.PrometheusRuleGroup,
pauseRecordingRules bool,
pauseAlertRules bool,
keepOriginalRuleDefinition bool,
notificationSettings []models.NotificationSettings,
logger log.Logger,
) (*models.AlertRuleGroup, error) {
logger.Info("Converting Prometheus rules to Grafana rules", "rules", len(promGroup.Rules), "folder_uid", namespaceUID, "datasource_uid", ds.UID, "datasource_type", ds.Type)
rules := make([]prom.PrometheusRule, len(promGroup.Rules))
for i, r := range promGroup.Rules {
rules[i] = prom.PrometheusRule{
Alert: r.Alert,
Expr: r.Expr,
For: r.For,
KeepFiringFor: r.KeepFiringFor,
Labels: r.Labels,
Annotations: r.Annotations,
Record: r.Record,
}
}
group := prom.PrometheusRuleGroup{
Name: promGroup.Name,
Interval: promGroup.Interval,
Rules: rules,
QueryOffset: promGroup.QueryOffset,
Limit: promGroup.Limit,
Labels: promGroup.Labels,
}
converter, err := prom.NewConverter(
prom.Config{
DatasourceUID: ds.UID,
DatasourceType: ds.Type,
TargetDatasourceUID: tds.UID,
TargetDatasourceType: tds.Type,
DefaultInterval: srv.cfg.DefaultRuleEvaluationInterval,
RecordingRules: prom.RulesConfig{
IsPaused: pauseRecordingRules,
},
AlertRules: prom.RulesConfig{
IsPaused: pauseAlertRules,
},
KeepOriginalRuleDefinition: util.Pointer(keepOriginalRuleDefinition),
EvaluationOffset: &srv.cfg.PrometheusConversion.RuleQueryOffset,
NotificationSettings: notificationSettings,
},
)
if err != nil {
logger.Error("Failed to create Prometheus converter", "datasource_uid", ds.UID, "datasource_type", ds.Type, "error", err)
return nil, err
}
grafanaGroup, err := converter.PrometheusRulesToGrafana(c.GetOrgID(), namespaceUID, group)
if err != nil {
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
return nil, err
}
return grafanaGroup, nil
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, amCfg apimodels.AlertmanagerUserConfig) response.Response {
if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
logger := srv.logger.FromContext(c.Req.Context())
identifier := parseConfigIdentifierHeader(c)
mergeMatchers, err := parseMergeMatchersHeader(c)
if err != nil {
logger.Error("Failed to parse merge matchers header", "error", err, "identifier", identifier)
return errorToResponse(err)
}
ec := apimodels.ExtraConfiguration{
Identifier: identifier,
MergeMatchers: mergeMatchers,
TemplateFiles: amCfg.TemplateFiles,
AlertmanagerConfig: amCfg.AlertmanagerConfig,
}
err = ec.Validate()
if err != nil {
logger.Error("Invalid alertmanager configuration", "error", err, "identifier", identifier)
return errorToResponse(err)
}
err = srv.am.SaveAndApplyExtraConfiguration(c.Req.Context(), c.GetOrgID(), ec)
if err != nil {
logger.Error("Failed to save alertmanager configuration", "error", err, "identifier", identifier)
return errorToResponse(fmt.Errorf("failed to save alertmanager configuration: %w", err))
}
logger.Info("Successfully updated alertmanager configuration with imported Prometheus config", "identifier", identifier)
return successfulResponse()
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
logger := srv.logger.FromContext(c.Req.Context())
ctx := c.Req.Context()
identifier := parseConfigIdentifierHeader(c)
cfg, err := srv.am.GetAlertmanagerConfiguration(ctx, c.GetOrgID(), false)
if err != nil {
logger.Error("failed to get alertmanager configuration", "err", err)
return errorToResponse(err)
}
var extraCfg *apimodels.ExtraConfiguration
for i := range cfg.ExtraConfigs {
if cfg.ExtraConfigs[i].Identifier == identifier {
extraCfg = &cfg.ExtraConfigs[i]
break
}
}
if extraCfg == nil {
return response.Error(http.StatusNotFound, "Alertmanager configuration not found", nil)
}
sanitizedConfig, err := extraCfg.GetSanitizedAlertmanagerConfigYAML()
if err != nil {
return response.Error(http.StatusBadRequest, "Invalid Alertmanager configuration format", err)
}
respBody := apimodels.AlertmanagerUserConfig{
AlertmanagerConfig: sanitizedConfig,
TemplateFiles: extraCfg.TemplateFiles,
}
resp := response.YAML(http.StatusOK, respBody)
resp.SetHeader(configIdentifierHeader, extraCfg.Identifier)
resp.SetHeader(mergeMatchersHeader, formatMergeMatchers(extraCfg.MergeMatchers))
return resp
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
logger := srv.logger.FromContext(c.Req.Context())
identifier := parseConfigIdentifierHeader(c)
err := srv.am.DeleteExtraConfiguration(c.Req.Context(), c.GetOrgID(), identifier)
if err != nil {
logger.Error("Failed to delete alertmanager configuration", "error", err, "identifier", identifier)
return errorToResponse(fmt.Errorf("failed to delete alertmanager configuration: %w", err))
}
logger.Info("Successfully deleted extra alertmanager configuration", "identifier", identifier)
return successfulResponse()
}
// parseBooleanHeader parses a boolean header value, returning an error if the header
// is present but invalid. If the header is not present, returns (false, nil).
func parseBooleanHeader(header string, headerName string) (bool, error) {
if header == "" {
return false, nil
}
val, err := strconv.ParseBool(header)
if err != nil {
return false, errInvalidHeaderValue(headerName, errors.New("must be 'true' or 'false'"))
}
return val, nil
}
func grafanaNamespacesToPrometheus(groups []models.AlertRuleGroupWithFolderFullpath) (map[string][]apimodels.PrometheusRuleGroup, error) {
result := map[string][]apimodels.PrometheusRuleGroup{}
for _, group := range groups {
// Since the folder can be nested but mimirtool does not support nested paths,
// we need to use only the last folder in the full path.
// For example, if the current working folder is "general" and the full path is "grafana/some folder/general/production",
// we should use the "production" folder.
folder := filepath.Base(group.FolderFullpath)
promGroup, err := grafanaRuleGroupToPrometheus(group.Title, group.Rules)
if err != nil {
return nil, err
}
result[folder] = append(result[folder], promGroup)
}
return result, nil
}
func grafanaRuleGroupToPrometheus(group string, rules []models.AlertRule) (apimodels.PrometheusRuleGroup, error) {
if len(rules) == 0 {
return apimodels.PrometheusRuleGroup{}, nil
}
interval := time.Duration(rules[0].IntervalSeconds) * time.Second
promGroup := apimodels.PrometheusRuleGroup{
Name: group,
Interval: prommodel.Duration(interval),
Rules: make([]apimodels.PrometheusRule, len(rules)),
}
for i, rule := range rules {
promDefinition, err := rule.PrometheusRuleDefinition()
if err != nil {
return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to get the Prometheus definition of the rule with UID %s: %w", rule.UID, err)
}
var r apimodels.PrometheusRule
if err := yaml.Unmarshal([]byte(promDefinition), &r); err != nil {
return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to unmarshal Prometheus rule definition of the rule with UID %s: %w", rule.UID, err)
}
promGroup.Rules[i] = r
}
return promGroup, nil
}
func successfulResponse() response.Response {
return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{
Status: "success",
})
}
// getWorkingFolderUID returns the value of the folderUIDHeader
// if present. Otherwise, it returns the UID of the root folder.
func getWorkingFolderUID(c *contextmodel.ReqContext) string {
folderUID := strings.TrimSpace(c.Req.Header.Get(folderUIDHeader))
if folderUID != "" {
return folderUID
}
return folder.RootFolderUID
}
func namespaceErrorResponse(err error) response.Response {
if errors.Is(err, dashboards.ErrFolderNotFound) {
return response.Empty(http.StatusNotFound)
}
return toNamespaceErrorResponse(err)
}
func promGroupHasRecordingRules(promGroup apimodels.PrometheusRuleGroup) bool {
for _, rule := range promGroup.Rules {
if rule.Record != "" {
return true
}
}
return false
}
// getProvenance determines the provenance value to use for rules created via the Prometheus conversion API.
// If the X-Disable-Provenance header is present in the request, returns ProvenanceNone,
// otherwise returns ProvenanceConvertedPrometheus.
func getProvenance(ctx *contextmodel.ReqContext) models.Provenance {
if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled {
return models.ProvenanceNone
}
return models.ProvenanceConvertedPrometheus
}
func parseNotificationSettingsHeader(ctx *contextmodel.ReqContext) ([]models.NotificationSettings, error) {
var notificationSettings []models.NotificationSettings
notificationSettingsJSON := ctx.Req.Header.Get(notificationSettingsHeader)
if notificationSettingsJSON != "" {
var settings apimodels.AlertRuleNotificationSettings
var err error
if err := json.Unmarshal([]byte(notificationSettingsJSON), &settings); err != nil {
return nil, errInvalidHeaderValue(notificationSettingsHeader, errors.New("invalid JSON"))
}
notificationSettings, err = validation.ValidateNotificationSettings(&settings)
if err != nil {
return nil, errInvalidHeaderValue(notificationSettingsHeader, err)
}
}
return notificationSettings, nil
}
// parseMergeMatchersHeader parses the merge matchers header value.
// Expected format: "key1=value1,key2=value2"
func parseMergeMatchersHeader(c *contextmodel.ReqContext) (amconfig.Matchers, error) {
matchersStr := strings.TrimSpace(c.Req.Header.Get(mergeMatchersHeader))
if matchersStr == "" {
return amconfig.Matchers{}, errInvalidHeaderValue(mergeMatchersHeader, errors.New("value cannot be empty"))
}
matchers := amconfig.Matchers{}
for pair := range strings.SplitSeq(matchersStr, ",") {
parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
if len(parts) != 2 {
return nil, errInvalidHeaderValue(mergeMatchersHeader, errors.New("format should be 'key=value,key2=value2'"))
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if key == "" || value == "" {
return nil, errInvalidHeaderValue(mergeMatchersHeader, errors.New("keys and values cannot be empty"))
}
matchers = append(matchers, &labels.Matcher{
Type: labels.MatchEqual,
Name: key,
Value: value,
})
}
return matchers, nil
}
func formatMergeMatchers(matchers amconfig.Matchers) string {
var pairs []string
for _, matcher := range matchers {
if matcher.Type == labels.MatchEqual {
pairs = append(pairs, fmt.Sprintf("%s=%s", matcher.Name, matcher.Value))
}
}
return strings.Join(pairs, ",")
}
func parseConfigIdentifierHeader(c *contextmodel.ReqContext) string {
identifier := strings.TrimSpace(c.Req.Header.Get(configIdentifierHeader))
if identifier == "" {
return defaultConfigIdentifier
}
return identifier
}