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 }