Alerting: Create endpoints for exporting in provisioning file format (#58623)

This adds provisioning endpoints for downloading alert rules and alert rule groups in a 
format that is compatible with file provisioning. Each endpoint supports both json and 
yaml response types via Accept header as well as a query parameter 
download=true/false that will set Content-Disposition to recommend initiating a download 
or inline display.

This also makes some package changes to keep structs with potential to drift closer 
together. Eventually, other alerting file structs should also move into this new file 
package, but the rest require some refactoring that is out of scope for this PR.
This commit is contained in:
Matthew Jacobson
2023-01-27 11:39:16 -05:00
committed by GitHub
parent d5294eb8fa
commit c006df375a
21 changed files with 2507 additions and 392 deletions
@@ -3,7 +3,9 @@ package api
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
@@ -12,6 +14,7 @@ import (
alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/provisioning/alerting/file"
"github.com/grafana/grafana/pkg/util"
)
@@ -60,6 +63,9 @@ type AlertRuleService interface {
DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error
GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (alerting_models.AlertRuleGroup, error)
ReplaceRuleGroup(ctx context.Context, orgID int64, group alerting_models.AlertRuleGroup, userID int64, provenance alerting_models.Provenance) error
GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (provisioning.AlertRuleWithFolderTitle, error)
GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, folder, group string) (file.AlertRuleGroupWithFolderTitle, error)
GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64) ([]file.AlertRuleGroupWithFolderTitle, error)
}
func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response {
@@ -334,6 +340,66 @@ func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *contextmodel.ReqContext, f
return response.JSON(http.StatusOK, definitions.NewAlertRuleGroupFromModel(g))
}
// RouteGetAlertRulesExport retrieves all alert rules in a format compatible with file provisioning.
func (srv *ProvisioningSrv) RouteGetAlertRulesExport(c *contextmodel.ReqContext) response.Response {
groupsWithTitle, err := srv.alertRules.GetAlertGroupsWithFolderTitle(c.Req.Context(), c.OrgID)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules")
}
e, err := file.NewAlertingFileExport(groupsWithTitle)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
}
return exportResponse(c, e)
}
// RouteGetAlertRuleGroupExport retrieves the given alert rule group in a format compatible with file provisioning.
func (srv *ProvisioningSrv) RouteGetAlertRuleGroupExport(c *contextmodel.ReqContext, folder string, group string) response.Response {
g, err := srv.alertRules.GetAlertRuleGroupWithFolderTitle(c.Req.Context(), c.OrgID, folder, group)
if err != nil {
if errors.Is(err, store.ErrAlertRuleGroupNotFound) {
return ErrResp(http.StatusNotFound, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "failed to get alert rule group")
}
e, err := file.NewAlertingFileExport([]file.AlertRuleGroupWithFolderTitle{g})
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
}
return exportResponse(c, e)
}
// RouteGetAlertRuleExport retrieves the given alert rule in a format compatible with file provisioning.
func (srv *ProvisioningSrv) RouteGetAlertRuleExport(c *contextmodel.ReqContext, UID string) response.Response {
rule, err := srv.alertRules.GetAlertRuleWithFolderTitle(c.Req.Context(), c.OrgID, UID)
if err != nil {
if errors.Is(err, alerting_models.ErrAlertRuleNotFound) {
return ErrResp(http.StatusNotFound, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "")
}
e, err := file.NewAlertingFileExport([]file.AlertRuleGroupWithFolderTitle{{
AlertRuleGroup: &alerting_models.AlertRuleGroup{
Title: rule.AlertRule.RuleGroup,
FolderUID: rule.AlertRule.NamespaceUID,
Interval: rule.AlertRule.IntervalSeconds,
Rules: []alerting_models.AlertRule{rule.AlertRule},
},
OrgID: c.OrgID,
FolderTitle: rule.FolderTitle,
}})
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
}
return exportResponse(c, e)
}
func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, ag definitions.AlertRuleGroup, folderUID string, group string) response.Response {
ag.FolderUID = folderUID
ag.Title = group
@@ -360,3 +426,26 @@ func determineProvenance(ctx *contextmodel.ReqContext) alerting_models.Provenanc
}
return alerting_models.ProvenanceAPI
}
func exportResponse(c *contextmodel.ReqContext, body any) response.Response {
format := "json"
acceptHeader := c.Req.Header.Get("Accept")
if strings.Contains(acceptHeader, "yaml") && !strings.Contains(acceptHeader, "json") {
format = "yaml"
}
download := c.QueryBoolWithDefault("download", false)
if download {
r := response.JSONDownload
if format == "yaml" {
r = response.YAMLDownload
}
return r(http.StatusOK, body, fmt.Sprintf("export.%s", format))
}
r := response.JSON
if format == "yaml" {
r = response.YAML
}
return r(http.StatusOK, body)
}
+461 -17
View File
@@ -5,18 +5,22 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
prometheus "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"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/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
@@ -370,17 +374,394 @@ func TestProvisioningApi(t *testing.T) {
})
})
})
t.Run("exports", func(t *testing.T) {
t.Run("alert rule group", func(t *testing.T) {
t.Run("are present, GET returns 200", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
require.Equal(t, 200, response.Status())
})
t.Run("are missing, GET returns 404", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "does not exist")
require.Equal(t, 404, response.Status())
})
t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Header.Add("Accept", "application/yaml")
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("accept header contains json, GET returns json", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Header.Add("Accept", "application/json")
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Header.Add("Accept", "application/json, application/yaml")
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Form.Set("download", "true")
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment")
})
t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Form.Set("download", "false")
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
})
t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
})
t.Run("json body content is as expected", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
insertRule(t, sut, createTestAlertRule("rule2", 1))
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}`
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body()))
})
t.Run("yaml body content is as expected", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
insertRule(t, sut, createTestAlertRule("rule2", 1))
rc.Context.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n"
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body()))
})
})
t.Run("alert rule", func(t *testing.T) {
t.Run("are present, GET returns 200", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
require.Equal(t, 200, response.Status())
})
t.Run("are missing, GET returns 404", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
response := sut.RouteGetAlertRuleExport(&rc, "rule404")
require.Equal(t, 404, response.Status())
})
t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
rc.Context.Req.Header.Add("Accept", "application/yaml")
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("accept header contains json, GET returns json", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
rc.Context.Req.Header.Add("Accept", "application/json")
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
rc.Context.Req.Header.Add("Accept", "application/json, application/yaml")
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
rc.Context.Req.Form.Set("download", "true")
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment")
})
t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
rc.Context.Req.Form.Set("download", "false")
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
})
t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
})
t.Run("json body content is as expected", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}`
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body()))
})
t.Run("yaml body content is as expected", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
rc.Context.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n"
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body()))
})
})
t.Run("all alert rules", func(t *testing.T) {
t.Run("are present, GET returns 200", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
})
t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Header.Add("Accept", "application/yaml")
response := sut.RouteGetAlertRulesExport(&rc)
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("accept header contains json, GET returns json", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Header.Add("Accept", "application/json")
response := sut.RouteGetAlertRulesExport(&rc)
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Header.Add("Accept", "application/json, application/yaml")
response := sut.RouteGetAlertRulesExport(&rc)
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
})
t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Form.Set("download", "true")
response := sut.RouteGetAlertRulesExport(&rc)
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment")
})
t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
rc.Context.Req.Form.Set("download", "false")
response := sut.RouteGetAlertRulesExport(&rc)
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
})
t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule", 1))
response := sut.RouteGetAlertRulesExport(&rc)
response.WriteTo(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
})
t.Run("json body content is as expected", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa"))
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb"))
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb"))
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}`
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body()))
})
t.Run("yaml body content is as expected", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa"))
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb"))
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb"))
rc.Context.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n"
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body()))
})
})
})
}
// testEnvironment binds together common dependencies for testing alerting APIs.
type testEnvironment struct {
secrets secrets.Service
log log.Logger
store store.DBstore
configs provisioning.AMConfigStore
xact provisioning.TransactionManager
quotas provisioning.QuotaChecker
prov provisioning.ProvisioningStore
secrets secrets.Service
log log.Logger
store store.DBstore
dashboardService dashboards.DashboardService
configs provisioning.AMConfigStore
xact provisioning.TransactionManager
quotas provisioning.QuotaChecker
prov provisioning.ProvisioningStore
}
func createTestEnv(t *testing.T) testEnvironment {
@@ -407,14 +788,29 @@ func createTestEnv(t *testing.T) testEnvironment {
prov.EXPECT().SaveSucceeds()
prov.EXPECT().GetReturns(models.ProvenanceNone)
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(&dashboards.Dashboard{
UID: "folder-uid",
Title: "Folder Title",
}, nil).Maybe()
dashboardService.On("GetDashboards", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardsQuery")).Return([]*dashboards.Dashboard{{
UID: "folder-uid",
Title: "Folder Title",
},
{
UID: "folder-uid2",
Title: "Folder Title2",
}}, nil).Maybe()
return testEnvironment{
secrets: secrets,
log: log,
configs: configs,
store: store,
xact: xact,
prov: prov,
quotas: quotas,
secrets: secrets,
log: log,
configs: configs,
store: store,
dashboardService: dashboardService,
xact: xact,
prov: prov,
quotas: quotas,
}
}
@@ -434,14 +830,18 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, env.log),
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log),
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.quotas, env.xact, 60, 10, env.log),
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, env.log),
}
}
func createTestRequestCtx() contextmodel.ReqContext {
return contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
Req: &http.Request{
Header: make(http.Header),
Form: make(url.Values),
},
Resp: web.NewResponseWriter("GET", httptest.NewRecorder()),
},
SignedInUser: &user.SignedInUser{
OrgID: 1,
@@ -555,15 +955,23 @@ func createInvalidAlertRuleGroup() definitions.AlertRuleGroup {
}
}
func createTestAlertRuleWithFolderAndGroup(title string, orgID int64, folderUid string, group string) definitions.ProvisionedAlertRule {
rule := createTestAlertRule(title, orgID)
rule.FolderUID = folderUid
rule.RuleGroup = group
return rule
}
func createTestAlertRule(title string, orgID int64) definitions.ProvisionedAlertRule {
return definitions.ProvisionedAlertRule{
UID: title,
OrgID: orgID,
Title: title,
Condition: "A",
Data: []models.AlertQuery{
{
RefID: "A",
Model: json.RawMessage("{}"),
Model: json.RawMessage(testModel),
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(60),
To: models.Duration(0),
@@ -600,6 +1008,42 @@ func deserializeRule(t *testing.T, data []byte) definitions.ProvisionedAlertRule
return rule
}
var testModel = `
{
"conditions": [
{
"evaluator": {
"params": [
3
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A"
]
},
"reducer": {
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "-100"
},
"expression": "1==0",
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "A",
"type": "math"
}
`
var testConfig = `
{
"template_files": {
+4 -1
View File
@@ -201,7 +201,10 @@ func (api *API) authorize(method, path string) web.Handler {
http.MethodGet + "/api/v1/provisioning/mute-timings/{name}",
http.MethodGet + "/api/v1/provisioning/alert-rules",
http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}",
http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}":
http.MethodGet + "/api/v1/provisioning/alert-rules/export",
http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}/export",
http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}",
http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export":
fallback = middleware.ReqOrgAdmin
eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope
@@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 41)
require.Len(t, paths, 44)
ac := acmock.New()
api := &API{AccessControl: ac}
@@ -24,8 +24,11 @@ type ProvisioningApi interface {
RouteDeleteMuteTiming(*contextmodel.ReqContext) response.Response
RouteDeleteTemplate(*contextmodel.ReqContext) response.Response
RouteGetAlertRule(*contextmodel.ReqContext) response.Response
RouteGetAlertRuleExport(*contextmodel.ReqContext) response.Response
RouteGetAlertRuleGroup(*contextmodel.ReqContext) response.Response
RouteGetAlertRuleGroupExport(*contextmodel.ReqContext) response.Response
RouteGetAlertRules(*contextmodel.ReqContext) response.Response
RouteGetAlertRulesExport(*contextmodel.ReqContext) response.Response
RouteGetContactpoints(*contextmodel.ReqContext) response.Response
RouteGetMuteTiming(*contextmodel.ReqContext) response.Response
RouteGetMuteTimings(*contextmodel.ReqContext) response.Response
@@ -69,15 +72,29 @@ func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *contextmodel.ReqContext)
uIDParam := web.Params(ctx.Req)[":UID"]
return f.handleRouteGetAlertRule(ctx, uIDParam)
}
func (f *ProvisioningApiHandler) RouteGetAlertRuleExport(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
uIDParam := web.Params(ctx.Req)[":UID"]
return f.handleRouteGetAlertRuleExport(ctx, uIDParam)
}
func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
folderUIDParam := web.Params(ctx.Req)[":FolderUID"]
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam)
}
func (f *ProvisioningApiHandler) RouteGetAlertRuleGroupExport(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
folderUIDParam := web.Params(ctx.Req)[":FolderUID"]
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteGetAlertRuleGroupExport(ctx, folderUIDParam, groupParam)
}
func (f *ProvisioningApiHandler) RouteGetAlertRules(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteGetAlertRules(ctx)
}
func (f *ProvisioningApiHandler) RouteGetAlertRulesExport(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteGetAlertRulesExport(ctx)
}
func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteGetContactpoints(ctx)
}
@@ -239,6 +256,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/alert-rules/{UID}/export"),
api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules/{UID}/export"),
metrics.Instrument(
http.MethodGet,
"/api/v1/provisioning/alert-rules/{UID}/export",
srv.RouteGetAlertRuleExport,
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"),
api.authorize(http.MethodGet, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"),
@@ -249,6 +276,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export"),
api.authorize(http.MethodGet, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export"),
metrics.Instrument(
http.MethodGet,
"/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export",
srv.RouteGetAlertRuleGroupExport,
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/alert-rules"),
api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules"),
@@ -259,6 +296,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/alert-rules/export"),
api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules/export"),
metrics.Instrument(
http.MethodGet,
"/api/v1/provisioning/alert-rules/export",
srv.RouteGetAlertRulesExport,
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/contact-points"),
api.authorize(http.MethodGet, "/api/v1/provisioning/contact-points"),
+12
View File
@@ -84,6 +84,14 @@ func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *contextmodel.ReqCo
return f.svc.RouteRouteGetAlertRule(ctx, UID)
}
func (f *ProvisioningApiHandler) handleRouteGetAlertRuleExport(ctx *contextmodel.ReqContext, UID string) response.Response {
return f.svc.RouteGetAlertRuleExport(ctx, UID)
}
func (f *ProvisioningApiHandler) handleRouteGetAlertRulesExport(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteGetAlertRulesExport(ctx)
}
func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *contextmodel.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response {
return f.svc.RoutePostAlertRule(ctx, ar)
}
@@ -104,6 +112,10 @@ func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *contextmodel.
return f.svc.RouteGetAlertRuleGroup(ctx, folder, group)
}
func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroupExport(ctx *contextmodel.ReqContext, folder, group string) response.Response {
return f.svc.RouteGetAlertRuleGroupExport(ctx, folder, group)
}
func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *contextmodel.ReqContext, ag apimodels.AlertRuleGroup, folder, group string) response.Response {
return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group)
}
+241 -3
View File
@@ -121,6 +121,28 @@
"title": "AlertQuery represents a single query associated with an alert definition.",
"type": "object"
},
"AlertQueryExport": {
"properties": {
"datasourceUid": {
"type": "string"
},
"model": {
"additionalProperties": {},
"type": "object"
},
"queryType": {
"type": "string"
},
"refId": {
"type": "string"
},
"relativeTimeRange": {
"$ref": "#/definitions/RelativeTimeRange"
}
},
"title": "AlertQueryExport is the provisioned export of models.AlertQuery.",
"type": "object"
},
"AlertResponse": {
"properties": {
"data": {
@@ -141,6 +163,65 @@
],
"type": "object"
},
"AlertRuleExport": {
"properties": {
"annotations": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"condition": {
"type": "string"
},
"dasboardUid": {
"type": "string"
},
"data": {
"items": {
"$ref": "#/definitions/AlertQueryExport"
},
"type": "array"
},
"execErrState": {
"enum": [
"Alerting",
"Error",
"OK"
],
"type": "string"
},
"for": {
"$ref": "#/definitions/Duration"
},
"labels": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"noDataState": {
"enum": [
"Alerting",
"NoData",
"OK"
],
"type": "string"
},
"panelId": {
"format": "int64",
"type": "integer"
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"title": "AlertRuleExport is the provisioned file export of models.AlertRule.",
"type": "object"
},
"AlertRuleGroup": {
"properties": {
"folderUid": {
@@ -162,6 +243,31 @@
},
"type": "object"
},
"AlertRuleGroupExport": {
"properties": {
"folder": {
"type": "string"
},
"interval": {
"$ref": "#/definitions/Duration"
},
"name": {
"type": "string"
},
"orgId": {
"format": "int64",
"type": "integer"
},
"rules": {
"items": {
"$ref": "#/definitions/AlertRuleExport"
},
"type": "array"
}
},
"title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.",
"type": "object"
},
"AlertRuleGroupMetadata": {
"properties": {
"interval": {
@@ -171,6 +277,22 @@
},
"type": "object"
},
"AlertingFileExport": {
"properties": {
"apiVersion": {
"format": "int64",
"type": "integer"
},
"groups": {
"items": {
"$ref": "#/definitions/AlertRuleGroupExport"
},
"type": "array"
}
},
"title": "AlertingFileExport is the full provisioned file export.",
"type": "object"
},
"AlertingRule": {
"description": "adapted from cortex",
"properties": {
@@ -3155,7 +3277,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\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 RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -3191,7 +3312,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"
},
"Userinfo": {
@@ -3551,6 +3672,7 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/definitions/gettableAlert"
},
@@ -3611,7 +3733,6 @@
"type": "array"
},
"integration": {
"description": "Integration integration",
"properties": {
"lastNotifyAttempt": {
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
@@ -3793,6 +3914,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"active": {
"description": "active",
@@ -3967,6 +4089,35 @@
]
}
},
"/api/v1/provisioning/alert-rules/export": {
"get": {
"operationId": "RouteGetAlertRulesExport",
"parameters": [
{
"default": false,
"description": "Whether to initiate a download of the file or not.",
"in": "query",
"name": "download",
"type": "boolean"
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Export all alert rules in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/alert-rules/{UID}": {
"delete": {
"operationId": "RouteDeleteAlertRule",
@@ -4062,6 +4213,47 @@
]
}
},
"/api/v1/provisioning/alert-rules/{UID}/export": {
"get": {
"operationId": "RouteGetAlertRuleExport",
"parameters": [
{
"description": "Alert rule UID",
"in": "path",
"name": "UID",
"required": true,
"type": "string"
},
{
"default": false,
"description": "Whether to initiate a download of the file or not.",
"in": "query",
"name": "download",
"type": "boolean"
}
],
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Export an alert rule in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/contact-points": {
"get": {
"operationId": "RouteGetContactpoints",
@@ -4265,6 +4457,52 @@
]
}
},
"/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": {
"get": {
"operationId": "RouteGetAlertRuleGroupExport",
"parameters": [
{
"in": "path",
"name": "FolderUID",
"required": true,
"type": "string"
},
{
"in": "path",
"name": "Group",
"required": true,
"type": "string"
},
{
"default": false,
"description": "Whether to initiate a download of the file or not.",
"in": "query",
"name": "download",
"type": "boolean"
}
],
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Export an alert rule group in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/mute-timings": {
"get": {
"operationId": "RouteGetMuteTimings",
@@ -4,6 +4,8 @@ import (
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/provisioning/alerting/file"
"github.com/prometheus/common/model"
)
@@ -14,6 +16,14 @@ import (
// Responses:
// 200: ProvisionedAlertRules
// swagger:route GET /api/v1/provisioning/alert-rules/export provisioning stable RouteGetAlertRulesExport
//
// Export all alert rules in provisioning file format.
//
// Responses:
// 200: AlertingFileExport
// 404: description: Not found.
// swagger:route GET /api/v1/provisioning/alert-rules/{UID} provisioning stable RouteGetAlertRule
//
// Get a specific alert rule by UID.
@@ -22,6 +32,19 @@ import (
// 200: ProvisionedAlertRule
// 404: description: Not found.
// swagger:route GET /api/v1/provisioning/alert-rules/{UID}/export provisioning stable RouteGetAlertRuleExport
//
// Export an alert rule in provisioning file format.
//
// Produces:
// - application/json
// - application/yaml
// - text/yaml
//
// Responses:
// 200: AlertingFileExport
// 404: description: Not found.
// swagger:route POST /api/v1/provisioning/alert-rules provisioning stable RoutePostAlertRule
//
// Create a new alert rule.
@@ -51,7 +74,7 @@ import (
// Responses:
// 204: description: The alert rule was deleted successfully.
// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule
// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule RouteGetAlertRuleExport
type AlertRuleUIDReference struct {
// Alert rule UID
// in:path
@@ -168,6 +191,19 @@ func NewAlertRules(rules []*models.AlertRule) ProvisionedAlertRules {
// 200: AlertRuleGroup
// 404: description: Not found.
// swagger:route GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export provisioning stable RouteGetAlertRuleGroupExport
//
// Export an alert rule group in provisioning file format.
//
// Produces:
// - application/json
// - application/yaml
// - text/yaml
//
// Responses:
// 200: AlertingFileExport
// 404: description: Not found.
// swagger:route PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RoutePutAlertRuleGroup
//
// Update the interval of a rule group.
@@ -179,13 +215,13 @@ func NewAlertRules(rules []*models.AlertRule) ProvisionedAlertRules {
// 200: AlertRuleGroup
// 400: ValidationError
// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup
// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport
type FolderUIDPathParam struct {
// in:path
FolderUID string `json:"FolderUID"`
}
// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup
// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport
type RuleGroupPathParam struct {
// in:path
Group string `json:"Group"`
@@ -202,6 +238,15 @@ type AlertRuleGroupMetadata struct {
Interval int64 `json:"interval"`
}
// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetAlertRulesExport
type ExportQueryParams struct {
// Whether to initiate a download of the file or not.
// in: query
// required: false
// default: false
Download bool `json:"download"`
}
// swagger:model
type AlertRuleGroup struct {
Title string `json:"title"`
@@ -210,6 +255,10 @@ type AlertRuleGroup struct {
Rules []ProvisionedAlertRule `json:"rules"`
}
// AlertingFileExport is the full provisioned file export.
// swagger:model
type AlertingFileExport = file.AlertingFileExport
func (a *AlertRuleGroup) ToModel() (models.AlertRuleGroup, error) {
ruleGroup := models.AlertRuleGroup{
Title: a.Title,
+237 -3
View File
@@ -121,6 +121,28 @@
"title": "AlertQuery represents a single query associated with an alert definition.",
"type": "object"
},
"AlertQueryExport": {
"properties": {
"datasourceUid": {
"type": "string"
},
"model": {
"additionalProperties": {},
"type": "object"
},
"queryType": {
"type": "string"
},
"refId": {
"type": "string"
},
"relativeTimeRange": {
"$ref": "#/definitions/RelativeTimeRange"
}
},
"title": "AlertQueryExport is the provisioned export of models.AlertQuery.",
"type": "object"
},
"AlertResponse": {
"properties": {
"data": {
@@ -141,6 +163,77 @@
],
"type": "object"
},
"AlertRuleExport": {
"properties": {
"annotations": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"condition": {
"type": "string"
},
"dasboardUid": {
"type": "string"
},
"data": {
"items": {
"$ref": "#/definitions/AlertQueryExport"
},
"type": "array"
},
"execErrState": {
"enum": [
"Alerting",
"Error",
"OK"
],
"type": "string"
},
"for": {
"$ref": "#/definitions/Duration"
},
"labels": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"noDataState": {
"enum": [
"Alerting",
"NoData",
"OK"
],
"type": "string"
},
"panelId": {
"format": "int64",
"type": "integer"
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"title": "AlertRuleExport is the provisioned export of models.AlertRule.",
"type": "object"
},
"AlertRuleFileExport": {
"properties": {
"groups": {
"items": {
"$ref": "#/definitions/AlertRuleGroupExport"
},
"type": "array"
}
},
"title": "AlertRuleFileExport is the provisioned export of multiple models.AlertRuleGroup.",
"type": "object"
},
"AlertRuleGroup": {
"properties": {
"folderUid": {
@@ -162,6 +255,31 @@
},
"type": "object"
},
"AlertRuleGroupExport": {
"properties": {
"folder": {
"type": "string"
},
"interval": {
"$ref": "#/definitions/Duration"
},
"name": {
"type": "string"
},
"orgId": {
"format": "int64",
"type": "integer"
},
"rules": {
"items": {
"$ref": "#/definitions/AlertRuleExport"
},
"type": "array"
}
},
"title": "AlertRuleGroupExport is the provisioned export of models.AlertRuleGroup.",
"type": "object"
},
"AlertRuleGroupMetadata": {
"properties": {
"interval": {
@@ -3155,6 +3273,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\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 RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -3190,7 +3309,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"
},
"Userinfo": {
@@ -3391,7 +3510,6 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup"
},
@@ -3496,7 +3614,6 @@
"type": "object"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/definitions/labelSet"
@@ -3558,6 +3675,7 @@
"type": "array"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": {
"comment": {
"description": "comment",
@@ -5640,6 +5758,35 @@
]
}
},
"/api/v1/provisioning/alert-rules/export": {
"get": {
"operationId": "RouteGetAlertRulesExport",
"parameters": [
{
"default": false,
"description": "Whether to initiate a download of the file or not.",
"in": "query",
"name": "download",
"type": "boolean"
}
],
"responses": {
"200": {
"description": "AlertRuleFileExport",
"schema": {
"$ref": "#/definitions/AlertRuleFileExport"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Export all alert rules in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/alert-rules/{UID}": {
"delete": {
"operationId": "RouteDeleteAlertRule",
@@ -5735,6 +5882,47 @@
]
}
},
"/api/v1/provisioning/alert-rules/{UID}/export": {
"get": {
"operationId": "RouteGetAlertRuleExport",
"parameters": [
{
"description": "Alert rule UID",
"in": "path",
"name": "UID",
"required": true,
"type": "string"
},
{
"default": false,
"description": "Whether to initiate a download of the file or not.",
"in": "query",
"name": "download",
"type": "boolean"
}
],
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"responses": {
"200": {
"description": "AlertRuleExport",
"schema": {
"$ref": "#/definitions/AlertRuleExport"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Export an alert rule in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/contact-points": {
"get": {
"operationId": "RouteGetContactpoints",
@@ -5938,6 +6126,52 @@
]
}
},
"/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": {
"get": {
"operationId": "RouteGetAlertRuleGroupExport",
"parameters": [
{
"in": "path",
"name": "FolderUID",
"required": true,
"type": "string"
},
{
"in": "path",
"name": "Group",
"required": true,
"type": "string"
},
{
"default": false,
"description": "Whether to initiate a download of the file or not.",
"in": "query",
"name": "download",
"type": "boolean"
}
],
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"responses": {
"200": {
"description": "AlertRuleGroupExport",
"schema": {
"$ref": "#/definitions/AlertRuleGroupExport"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Export an alert rule group in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/mute-timings": {
"get": {
"operationId": "RouteGetMuteTimings",
+243 -4
View File
@@ -1746,6 +1746,36 @@
}
}
},
"/api/v1/provisioning/alert-rules/export": {
"get": {
"tags": [
"provisioning",
"stable"
],
"summary": "Export all alert rules in provisioning file format.",
"operationId": "RouteGetAlertRulesExport",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "Whether to initiate a download of the file or not.",
"name": "download",
"in": "query"
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/api/v1/provisioning/alert-rules/{UID}": {
"get": {
"tags": [
@@ -1844,6 +1874,48 @@
}
}
},
"/api/v1/provisioning/alert-rules/{UID}/export": {
"get": {
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"tags": [
"provisioning",
"stable"
],
"summary": "Export an alert rule in provisioning file format.",
"operationId": "RouteGetAlertRuleExport",
"parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
},
{
"type": "boolean",
"default": false,
"description": "Whether to initiate a download of the file or not.",
"name": "download",
"in": "query"
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/api/v1/provisioning/contact-points": {
"get": {
"tags": [
@@ -2053,6 +2125,53 @@
}
}
},
"/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": {
"get": {
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"tags": [
"provisioning",
"stable"
],
"summary": "Export an alert rule group in provisioning file format.",
"operationId": "RouteGetAlertRuleGroupExport",
"parameters": [
{
"type": "string",
"name": "FolderUID",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
},
{
"type": "boolean",
"default": false,
"description": "Whether to initiate a download of the file or not.",
"name": "download",
"in": "query"
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/api/v1/provisioning/mute-timings": {
"get": {
"tags": [
@@ -2612,6 +2731,28 @@
}
}
},
"AlertQueryExport": {
"type": "object",
"title": "AlertQueryExport is the provisioned export of models.AlertQuery.",
"properties": {
"datasourceUid": {
"type": "string"
},
"model": {
"type": "object",
"additionalProperties": {}
},
"queryType": {
"type": "string"
},
"refId": {
"type": "string"
},
"relativeTimeRange": {
"$ref": "#/definitions/RelativeTimeRange"
}
}
},
"AlertResponse": {
"type": "object",
"required": [
@@ -2632,6 +2773,65 @@
}
}
},
"AlertRuleExport": {
"type": "object",
"title": "AlertRuleExport is the provisioned file export of models.AlertRule.",
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"condition": {
"type": "string"
},
"dasboardUid": {
"type": "string"
},
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertQueryExport"
}
},
"execErrState": {
"type": "string",
"enum": [
"Alerting",
"Error",
"OK"
]
},
"for": {
"$ref": "#/definitions/Duration"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"noDataState": {
"type": "string",
"enum": [
"Alerting",
"NoData",
"OK"
]
},
"panelId": {
"type": "integer",
"format": "int64"
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
"AlertRuleGroup": {
"type": "object",
"properties": {
@@ -2653,6 +2853,31 @@
}
}
},
"AlertRuleGroupExport": {
"type": "object",
"title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.",
"properties": {
"folder": {
"type": "string"
},
"interval": {
"$ref": "#/definitions/Duration"
},
"name": {
"type": "string"
},
"orgId": {
"type": "integer",
"format": "int64"
},
"rules": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertRuleExport"
}
}
}
},
"AlertRuleGroupMetadata": {
"type": "object",
"properties": {
@@ -2662,6 +2887,23 @@
}
}
},
"AlertingFileExport": {
"type": "object",
"title": "AlertingFileExport is the full provisioned file export.",
"properties": {
"apiVersion": {
"type": "integer",
"format": "int64"
},
"groups": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertRuleGroupExport"
}
}
},
"$ref": "#/definitions/AlertingFileExport"
},
"AlertingRule": {
"description": "adapted from cortex",
"type": "object",
@@ -5887,7 +6129,6 @@
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"$ref": "#/definitions/alertGroup"
@@ -5993,7 +6234,6 @@
}
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object",
"required": [
"labels",
@@ -6057,6 +6297,7 @@
"$ref": "#/definitions/gettableAlerts"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object",
"required": [
"comment",
@@ -6113,7 +6354,6 @@
"$ref": "#/definitions/gettableSilences"
},
"integration": {
"description": "Integration integration",
"type": "object",
"required": [
"name",
@@ -6296,7 +6536,6 @@
"$ref": "#/definitions/postableSilence"
},
"receiver": {
"description": "Receiver receiver",
"type": "object",
"required": [
"active",