mirror of https://github.com/grafana/grafana.git
Alerting: API to convert submitted Prometheus rules to GMA (#102231)
* placeholder commit * Complete function in api_convert_prometheus.go * MVP before extensive testing * Cleanup * Updated tests * cleanup * Fix random logs and lint * Remove comment * Fix errors after rebase * Update test * Update swagger * swagger * Refactor to accept groups in body * Fix auth tests and some cleanup * Some cleanup before refactoring * Remove unnecessary fields * Also refactor RouteConvertPrometheusPostRuleGroup * Remove unused code * Rebase + cleanup * Update authorization_test * address comments * Regen swagger files * Remove namespace and group filters * Final comments
This commit is contained in:
parent
d7be68ab3c
commit
68d8810ecb
|
|
@ -320,30 +320,24 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmo
|
|||
// 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("folder_title", namespaceTitle, "group", promGroup.Name, "working_folder_uid", workingFolderUID)
|
||||
logger = logger.New("working_folder_uid", workingFolderUID)
|
||||
|
||||
// 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(promGroup) {
|
||||
if !srv.cfg.RecordingRules.Enabled {
|
||||
logger.Error("Cannot import recording rules", "error", errRecordingRulesNotEnabled)
|
||||
return errorToResponse(errRecordingRulesNotEnabled)
|
||||
}
|
||||
|
||||
if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagGrafanaManagedRecordingRulesDatasources) {
|
||||
logger.Error("Cannot import recording rules", "error", errRecordingRulesDatasourcesNotEnabled)
|
||||
return errorToResponse(errRecordingRulesDatasourcesNotEnabled)
|
||||
}
|
||||
pauseRecordingRules, err := parseBooleanHeader(c.Req.Header.Get(recordingRulesPausedHeader), recordingRulesPausedHeader)
|
||||
if err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
logger.Info("Converting Prometheus rule group", "rules", len(promGroup.Rules))
|
||||
|
||||
ns, errResp := srv.getOrCreateNamespace(c, namespaceTitle, logger, workingFolderUID)
|
||||
if errResp != nil {
|
||||
return errResp
|
||||
pauseAlertRules, err := parseBooleanHeader(c.Req.Header.Get(alertRulesPausedHeader), alertRulesPausedHeader)
|
||||
if err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
datasourceUID := strings.TrimSpace(c.Req.Header.Get(datasourceUIDHeader))
|
||||
|
|
@ -375,15 +369,44 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm
|
|||
// to ensure we can return them in this API in Prometheus format.
|
||||
keepOriginalRuleDefinition := provenance == models.ProvenanceConvertedPrometheus
|
||||
|
||||
group, err := srv.convertToGrafanaRuleGroup(c, ds, tds, ns.UID, promGroup, keepOriginalRuleDefinition, logger)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Prometheus rules to Grafana rules", "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)
|
||||
}
|
||||
|
||||
if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagGrafanaManagedRecordingRulesDatasources) {
|
||||
logger.Error("Cannot import recording rules", "error", errRecordingRulesDatasourcesNotEnabled)
|
||||
return errorToResponse(errRecordingRulesDatasourcesNotEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
grafanaGroup, err := srv.convertToGrafanaRuleGroup(c, ds, tds, namespace.UID, rg, pauseRecordingRules, pauseAlertRules, keepOriginalRuleDefinition, logger)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
grafanaGroups = append(grafanaGroups, grafanaGroup)
|
||||
}
|
||||
}
|
||||
|
||||
err = srv.alertRuleService.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, *group, provenance)
|
||||
// 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 group", "error", err)
|
||||
logger.Error("Failed to replace rule groups", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +439,8 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
|
|||
tds *datasources.DataSource,
|
||||
namespaceUID string,
|
||||
promGroup apimodels.PrometheusRuleGroup,
|
||||
pauseRecordingRules bool,
|
||||
pauseAlertRules bool,
|
||||
keepOriginalRuleDefinition bool,
|
||||
logger log.Logger,
|
||||
) (*models.AlertRuleGroup, error) {
|
||||
|
|
@ -439,16 +464,6 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
|
|||
Rules: rules,
|
||||
}
|
||||
|
||||
pauseRecordingRules, err := parseBooleanHeader(c.Req.Header.Get(recordingRulesPausedHeader), recordingRulesPausedHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pauseAlertRules, err := parseBooleanHeader(c.Req.Header.Get(alertRulesPausedHeader), alertRulesPausedHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
converter, err := prom.NewConverter(
|
||||
prom.Config{
|
||||
DatasourceUID: ds.UID,
|
||||
|
|
|
|||
|
|
@ -987,6 +987,200 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) {
|
||||
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
|
||||
|
||||
req := createRequestCtx()
|
||||
req.Req.Header.Set(datasourceUIDHeader, existingDSUID)
|
||||
|
||||
// Create test prometheus rules
|
||||
promAlertRule := apimodels.PrometheusRule{
|
||||
Alert: "TestAlert",
|
||||
Expr: "up == 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
}
|
||||
|
||||
promRecordingRule := apimodels.PrometheusRule{
|
||||
Record: "TestRecordingRule",
|
||||
Expr: "up == 0",
|
||||
}
|
||||
|
||||
promGroup1 := apimodels.PrometheusRuleGroup{
|
||||
Name: "TestGroup1",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{promAlertRule},
|
||||
}
|
||||
|
||||
promGroup2 := apimodels.PrometheusRuleGroup{
|
||||
Name: "TestGroup2",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{promAlertRule},
|
||||
}
|
||||
|
||||
promGroup3 := apimodels.PrometheusRuleGroup{
|
||||
Name: "TestGroup3",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{promAlertRule, promRecordingRule},
|
||||
}
|
||||
|
||||
promGroups := map[string][]apimodels.PrometheusRuleGroup{
|
||||
"namespace1": {promGroup1, promGroup2},
|
||||
"namespace2": {promGroup3},
|
||||
}
|
||||
|
||||
t.Run("should convert prometheus rules to Grafana rules", func(t *testing.T) {
|
||||
// Call the endpoint
|
||||
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
|
||||
// Verify the rules were created
|
||||
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
||||
OrgID: req.SignedInUser.GetOrgID(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 4)
|
||||
|
||||
// Verify rule content
|
||||
for _, rule := range rules {
|
||||
require.Equal(t, int64(60), rule.IntervalSeconds) // 1 minute interval
|
||||
|
||||
// Check that the rule matches one of our original prometheus rules
|
||||
switch rule.RuleGroup {
|
||||
case "TestGroup1":
|
||||
require.Equal(t, "TestAlert", rule.Title)
|
||||
require.Equal(t, "critical", rule.Labels["severity"])
|
||||
require.Equal(t, 5*time.Minute, rule.For)
|
||||
case "TestGroup2":
|
||||
require.Equal(t, "TestAlert", rule.Title)
|
||||
require.Equal(t, "critical", rule.Labels["severity"])
|
||||
require.Equal(t, 5*time.Minute, rule.For)
|
||||
case "TestGroup3":
|
||||
switch rule.Title {
|
||||
case "TestAlert":
|
||||
require.Equal(t, "critical", rule.Labels["severity"])
|
||||
require.Equal(t, 5*time.Minute, rule.For)
|
||||
case "TestRecordingRule":
|
||||
require.Equal(t, "TestRecordingRule", rule.Record.Metric)
|
||||
default:
|
||||
t.Fatalf("unexpected rule title: %s", rule.Title)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected rule group: %s", rule.RuleGroup)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should convert Prometheus rules to Grafana rules but pause recording rules", func(t *testing.T) {
|
||||
clear(ruleStore.Rules)
|
||||
|
||||
req.Req.Header.Set(alertRulesPausedHeader, "false")
|
||||
req.Req.Header.Set(recordingRulesPausedHeader, "true")
|
||||
|
||||
// Call the endpoint
|
||||
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
|
||||
// Verify the rules were created
|
||||
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
||||
OrgID: req.SignedInUser.GetOrgID(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 4)
|
||||
|
||||
// Verify the recording rule is paused
|
||||
for _, rule := range rules {
|
||||
if rule.Record != nil {
|
||||
require.True(t, rule.IsPaused)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should convert Prometheus rules to Grafana rules but pause alert rules", func(t *testing.T) {
|
||||
clear(ruleStore.Rules)
|
||||
|
||||
req.Req.Header.Set(alertRulesPausedHeader, "true")
|
||||
req.Req.Header.Set(recordingRulesPausedHeader, "false")
|
||||
|
||||
// Call the endpoint
|
||||
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
|
||||
// Verify the rules were created
|
||||
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
||||
OrgID: req.SignedInUser.GetOrgID(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 4)
|
||||
|
||||
// Verify the alert rule is paused
|
||||
for _, rule := range rules {
|
||||
if rule.Record == nil {
|
||||
require.True(t, rule.IsPaused)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should convert Prometheus rules to Grafana rules but pause both alert and recording rules", func(t *testing.T) {
|
||||
clear(ruleStore.Rules)
|
||||
|
||||
req.Req.Header.Set(recordingRulesPausedHeader, "true")
|
||||
req.Req.Header.Set(alertRulesPausedHeader, "true")
|
||||
|
||||
// Call the endpoint
|
||||
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
|
||||
// Verify the rules were created
|
||||
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
||||
OrgID: req.SignedInUser.GetOrgID(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 4)
|
||||
|
||||
// Verify the alert rule is paused
|
||||
for _, rule := range rules {
|
||||
require.True(t, rule.IsPaused)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("convert Prometheus rules to Grafana rules into a specified target folder", func(t *testing.T) {
|
||||
clear(ruleStore.Rules)
|
||||
|
||||
// Create a target folder to move the rules into
|
||||
fldr := randFolder()
|
||||
fldr.ParentUID = ""
|
||||
folderService.ExpectedFolder = fldr
|
||||
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
||||
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
||||
|
||||
req.Req.Header.Del(recordingRulesPausedHeader)
|
||||
req.Req.Header.Del(alertRulesPausedHeader)
|
||||
req.Req.Header.Set(folderUIDHeader, fldr.UID)
|
||||
|
||||
// Call the endpoint
|
||||
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
|
||||
// Verify the rules were created
|
||||
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
||||
OrgID: req.SignedInUser.GetOrgID(),
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 4)
|
||||
|
||||
for _, rule := range rules {
|
||||
parentFolders, err := folderService.GetParents(context.Background(), folder.GetParentsQuery{UID: rule.NamespaceUID, OrgID: 1})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, parentFolders, 1)
|
||||
require.Equal(t, fldr.UID, parentFolders[0].UID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type convertPrometheusSrvOptions struct {
|
||||
provenanceStore provisioning.ProvisioningStore
|
||||
fakeAccessControlRuleService *acfakes.FakeRuleService
|
||||
|
|
|
|||
|
|
@ -128,7 +128,9 @@ func (api *API) authorize(method, path string) web.Handler {
|
|||
)
|
||||
|
||||
case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
|
||||
http.MethodPost + "/api/convert/api/prom/rules/{NamespaceTitle}":
|
||||
http.MethodPost + "/api/convert/api/prom/rules/{NamespaceTitle}",
|
||||
http.MethodPost + "/api/convert/prometheus/config/v1/rules",
|
||||
http.MethodPost + "/api/convert/api/prom/config/v1/rules":
|
||||
eval = ac.EvalAll(
|
||||
ac.EvalPermission(ac.ActionAlertingRuleCreate),
|
||||
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) {
|
|||
}
|
||||
paths[p] = methods
|
||||
}
|
||||
require.Len(t, paths, 63)
|
||||
require.Len(t, paths, 64)
|
||||
|
||||
ac := acmock.New()
|
||||
api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ type ConvertPrometheusApi interface {
|
|||
RouteConvertPrometheusCortexGetRuleGroup(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusCortexPostRuleGroups(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response
|
||||
RouteConvertPrometheusPostRuleGroups(*contextmodel.ReqContext) response.Response
|
||||
}
|
||||
|
||||
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
|
||||
|
|
@ -63,6 +65,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup(
|
|||
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
|
||||
return f.handleRouteConvertPrometheusCortexPostRuleGroup(ctx, namespaceTitleParam)
|
||||
}
|
||||
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.handleRouteConvertPrometheusCortexPostRuleGroups(ctx)
|
||||
}
|
||||
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
|
||||
// Parse Path Parameters
|
||||
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
|
||||
|
|
@ -93,6 +98,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *c
|
|||
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
|
||||
return f.handleRouteConvertPrometheusPostRuleGroup(ctx, namespaceTitleParam)
|
||||
}
|
||||
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.handleRouteConvertPrometheusPostRuleGroups(ctx)
|
||||
}
|
||||
|
||||
func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m *metrics.API) {
|
||||
api.RouteRegister.Group("", func(group routing.RouteRegister) {
|
||||
|
|
@ -168,6 +176,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi,
|
|||
m,
|
||||
),
|
||||
)
|
||||
group.Post(
|
||||
toMacaronPath("/api/convert/api/prom/config/v1/rules"),
|
||||
requestmeta.SetOwner(requestmeta.TeamAlerting),
|
||||
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
|
||||
api.authorize(http.MethodPost, "/api/convert/api/prom/config/v1/rules"),
|
||||
metrics.Instrument(
|
||||
http.MethodPost,
|
||||
"/api/convert/api/prom/config/v1/rules",
|
||||
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexPostRuleGroups),
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Delete(
|
||||
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
|
||||
requestmeta.SetOwner(requestmeta.TeamAlerting),
|
||||
|
|
@ -240,5 +260,17 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi,
|
|||
m,
|
||||
),
|
||||
)
|
||||
group.Post(
|
||||
toMacaronPath("/api/convert/prometheus/config/v1/rules"),
|
||||
requestmeta.SetOwner(requestmeta.TeamAlerting),
|
||||
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
|
||||
api.authorize(http.MethodPost, "/api/convert/prometheus/config/v1/rules"),
|
||||
metrics.Instrument(
|
||||
http.MethodPost,
|
||||
"/api/convert/prometheus/config/v1/rules",
|
||||
api.Hooks.Wrap(srv.RouteConvertPrometheusPostRuleGroups),
|
||||
m,
|
||||
),
|
||||
)
|
||||
}, middleware.ReqSignedIn)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,44 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup(
|
|||
return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup)
|
||||
}
|
||||
|
||||
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
defer func() { _ = ctx.Req.Body.Close() }()
|
||||
|
||||
var m string
|
||||
|
||||
// Parse content-type only if it's not empty,
|
||||
// otherwise we'll assume it's yaml
|
||||
contentType := ctx.Req.Header.Get("content-type")
|
||||
if contentType != "" {
|
||||
m, _, err = mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
}
|
||||
|
||||
var promNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||
|
||||
switch m {
|
||||
case "application/yaml", "":
|
||||
// mimirtool does not send content-type, so if it's empty, we assume it's yaml
|
||||
if err := yaml.Unmarshal(body, &promNamespaces); err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
case "application/json":
|
||||
if err := json.Unmarshal(body, &promNamespaces); err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
default:
|
||||
return errorToResponse(errorUnsupportedMediaType.Errorf("unsupported media type: %s, only application/yaml and application/json are supported", m))
|
||||
}
|
||||
|
||||
return f.svc.RouteConvertPrometheusPostRuleGroups(ctx, promNamespaces)
|
||||
}
|
||||
|
||||
// cortextool
|
||||
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.handleRouteConvertPrometheusGetRules(ctx)
|
||||
|
|
@ -107,3 +145,7 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRuleG
|
|||
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
|
||||
return f.handleRouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle)
|
||||
}
|
||||
|
||||
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.handleRouteConvertPrometheusPostRuleGroups(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4470,7 +4470,6 @@
|
|||
"type": "object"
|
||||
},
|
||||
"URL": {
|
||||
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
|
||||
"properties": {
|
||||
"ForceQuery": {
|
||||
"type": "boolean"
|
||||
|
|
@ -4506,7 +4505,7 @@
|
|||
"$ref": "#/definitions/Userinfo"
|
||||
}
|
||||
},
|
||||
"title": "A URL represents a parsed URL (technically, a URI reference).",
|
||||
"title": "URL is a custom URL type that allows validation at configuration load time.",
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateRuleGroupResponse": {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,36 @@ import (
|
|||
// 403: ForbiddenError
|
||||
// 404: NotFound
|
||||
|
||||
// swagger:route POST /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusPostRuleGroups
|
||||
//
|
||||
// Converts the submitted rule groups into Grafana-Managed Rules.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
// - application/yaml
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: ConvertPrometheusResponse
|
||||
// 403: ForbiddenError
|
||||
|
||||
// swagger:route POST /convert/api/prom/config/v1/rules convert_prometheus RouteConvertPrometheusCortexPostRuleGroups
|
||||
//
|
||||
// Converts the submitted rule groups into Grafana-Managed Rules.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
// - application/yaml
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: ConvertPrometheusResponse
|
||||
// 403: ForbiddenError
|
||||
|
||||
// Route for mimirtool
|
||||
// swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup
|
||||
//
|
||||
|
|
|
|||
|
|
@ -4470,6 +4470,7 @@
|
|||
"type": "object"
|
||||
},
|
||||
"URL": {
|
||||
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
|
||||
"properties": {
|
||||
"ForceQuery": {
|
||||
"type": "boolean"
|
||||
|
|
@ -4505,7 +4506,7 @@
|
|||
"$ref": "#/definitions/Userinfo"
|
||||
}
|
||||
},
|
||||
"title": "URL is a custom URL type that allows validation at configuration load time.",
|
||||
"title": "A URL represents a parsed URL (technically, a URI reference).",
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateRuleGroupResponse": {
|
||||
|
|
@ -5032,6 +5033,7 @@
|
|||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence",
|
||||
"type": "object"
|
||||
|
|
@ -6374,6 +6376,36 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/convert/api/prom/config/v1/rules": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"application/yaml"
|
||||
],
|
||||
"operationId": "RouteConvertPrometheusCortexPostRuleGroups",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "ConvertPrometheusResponse",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ConvertPrometheusResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "ForbiddenError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ForbiddenError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Converts the submitted rule groups into Grafana-Managed Rules.",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/convert/api/prom/rules": {
|
||||
"get": {
|
||||
"operationId": "RouteConvertPrometheusCortexGetRules",
|
||||
|
|
@ -6651,6 +6683,34 @@
|
|||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"application/yaml"
|
||||
],
|
||||
"operationId": "RouteConvertPrometheusPostRuleGroups",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "ConvertPrometheusResponse",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ConvertPrometheusResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "ForbiddenError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ForbiddenError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Converts the submitted rule groups into Grafana-Managed Rules.",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/convert/prometheus/config/v1/rules/{NamespaceTitle}": {
|
||||
|
|
|
|||
|
|
@ -1102,6 +1102,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/convert/api/prom/config/v1/rules": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"application/yaml"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Converts the submitted rule groups into Grafana-Managed Rules.",
|
||||
"operationId": "RouteConvertPrometheusCortexPostRuleGroups",
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "ConvertPrometheusResponse",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ConvertPrometheusResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "ForbiddenError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ForbiddenError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/convert/api/prom/rules": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
|
@ -1379,6 +1409,34 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"application/yaml"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Converts the submitted rule groups into Grafana-Managed Rules.",
|
||||
"operationId": "RouteConvertPrometheusPostRuleGroups",
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "ConvertPrometheusResponse",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ConvertPrometheusResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "ForbiddenError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ForbiddenError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/convert/prometheus/config/v1/rules/{NamespaceTitle}": {
|
||||
|
|
@ -8546,8 +8604,9 @@
|
|||
}
|
||||
},
|
||||
"URL": {
|
||||
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
|
||||
"type": "object",
|
||||
"title": "URL is a custom URL type that allows validation at configuration load time.",
|
||||
"title": "A URL represents a parsed URL (technically, a URI reference).",
|
||||
"properties": {
|
||||
"ForceQuery": {
|
||||
"type": "boolean"
|
||||
|
|
@ -9108,6 +9167,7 @@
|
|||
}
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -203,7 +203,6 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom
|
|||
if err != nil {
|
||||
return models.AlertRule{}, err
|
||||
}
|
||||
|
||||
if isRecordingRule {
|
||||
record = &models.Record{
|
||||
From: queryRefID,
|
||||
|
|
|
|||
|
|
@ -450,6 +450,20 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden
|
|||
return service.persistDelta(ctx, user, delta, provenance)
|
||||
}
|
||||
|
||||
func (service *AlertRuleService) ReplaceRuleGroups(ctx context.Context, user identity.Requester, groups []*models.AlertRuleGroup, provenance models.Provenance) error {
|
||||
err := service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
for _, group := range groups {
|
||||
err := service.ReplaceRuleGroup(ctx, user, *group, provenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string, provenance models.Provenance) error {
|
||||
return service.DeleteRuleGroups(ctx, user, provenance, &FilterOptions{
|
||||
NamespaceUIDs: []string{namespaceUID},
|
||||
|
|
|
|||
Loading…
Reference in New Issue