diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 3dd41f82b2a..11e353dbaf0 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -65,7 +65,7 @@ type AlertRuleService interface { 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) (alerting_models.AlertRuleGroupWithFolderTitle, error) - GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64) ([]alerting_models.AlertRuleGroupWithFolderTitle, error) + GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64, folderUID string) ([]alerting_models.AlertRuleGroupWithFolderTitle, error) } func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response { @@ -390,7 +390,16 @@ func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *contextmodel.ReqContext, f // 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) + folderUID := c.Query("folderUid") + group := c.Query("group") + if group != "" { + if folderUID == "" { + return ErrResp(http.StatusBadRequest, nil, "group name must be specified together with folder_uid parameter") + } + return srv.RouteGetAlertRuleGroupExport(c, folderUID, group) + } + + groupsWithTitle, err := srv.alertRules.GetAlertGroupsWithFolderTitle(c.Req.Context(), c.OrgID, folderUID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") } diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 23c83cd7bbe..d856bc805e7 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -813,6 +813,51 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 200, response.Status()) require.Equal(t, expectedResponse, string(response.Body())) }) + + t.Run("accept query parameter folder_uid", 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/json") + rc.Context.Req.Form.Set("folderUid", "folder-uid") + 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":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"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":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + + t.Run("accepts parameter group", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + 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 := createTestRequestCtx() + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("folderUid", "folder-uid") + rc.Context.Req.Form.Set("group", "groupa") + + 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":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + + t.Run("and fails if folderUID is empty", func(t *testing.T) { + rc := createTestRequestCtx() + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("group", "groupa") + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 400, response.Status()) + }) + }) }) t.Run("notification policies", func(t *testing.T) { diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index cd9a2f81c70..c84c77b6129 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -3851,6 +3851,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 the 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" @@ -3886,7 +3887,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": { @@ -4066,7 +4067,6 @@ "type": "object" }, "alertGroup": { - "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -4090,6 +4090,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -4249,13 +4250,13 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -4493,6 +4494,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "active": { "description": "active", @@ -4684,6 +4686,18 @@ "in": "query", "name": "format", "type": "string" + }, + { + "description": "UID of folder from which export rules", + "in": "query", + "name": "folderUid", + "type": "string" + }, + { + "description": "Name of group of rules to export. Must be specified only together with folder UID", + "in": "query", + "name": "group", + "type": "string" } ], "responses": { diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning.go b/pkg/services/ngalert/api/tooling/definitions/provisioning.go index df5695b68a6..d4a06d9469e 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning.go @@ -9,7 +9,7 @@ type AlertingFileExport struct { Policies []NotificationPolicyExport `json:"policies,omitempty" yaml:"policies,omitempty"` } -// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetAlertRulesExport RouteGetContactpointsExport RouteGetContactpointExport +// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetContactpointsExport RouteGetContactpointExport type ExportQueryParams struct { // Whether to initiate a download of the file or not. // in: query diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index 558756873da..71d2835ea00 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -71,6 +71,20 @@ import ( // Responses: // 204: description: The alert rule was deleted successfully. +// swagger:parameters RouteGetAlertRulesExport +type AlertRulesExportParameters struct { + ExportQueryParams + // UID of folder from which export rules + // in:query + // required:false + FolderUID string `json:"folderUid"` + + // Name of group of rules to export. Must be specified only together with folder UID + // in:query + // required: false + GroupName string `json:"group"` +} + // swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule RouteGetAlertRuleExport type AlertRuleUIDReference struct { // Alert rule UID diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index cfc340084f4..c5fff6f18c1 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -4066,7 +4066,6 @@ "type": "object" }, "alertGroup": { - "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -4195,7 +4194,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -4251,14 +4249,12 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, "type": "array" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -4307,7 +4303,6 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, @@ -6469,6 +6464,18 @@ "in": "query", "name": "format", "type": "string" + }, + { + "description": "UID of folder from which export rules", + "in": "query", + "name": "folderUid", + "type": "string" + }, + { + "description": "Name of group of rules to export. Must be specified only together with folder UID", + "in": "query", + "name": "group", + "type": "string" } ], "responses": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index def69e901d1..3010a0b1153 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1877,6 +1877,18 @@ "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", "name": "format", "in": "query" + }, + { + "type": "string", + "description": "UID of folder from which export rules", + "name": "folderUid", + "in": "query" + }, + { + "type": "string", + "description": "Name of group of rules to export. Must be specified only together with folder UID", + "name": "group", + "in": "query" } ], "responses": { @@ -6913,7 +6925,6 @@ } }, "alertGroup": { - "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -7044,7 +7055,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -7101,7 +7111,6 @@ "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -7109,7 +7118,6 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -7159,7 +7167,6 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 8904ed50100..524a0a0bc0e 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -452,11 +452,14 @@ func (service *AlertRuleService) GetAlertRuleGroupWithFolderTitle(ctx context.Co return res, nil } -// GetAlertGroupsWithFolderTitle returns all groups with folder title that have at least one alert. -func (service *AlertRuleService) GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64) ([]models.AlertRuleGroupWithFolderTitle, error) { +// GetAlertGroupsWithFolderTitle returns all groups with folder title in the folder identified by folderUID that have at least one alert. If argument folderUID is an empty string - returns groups in all folders. +func (service *AlertRuleService) GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64, folderUID string) ([]models.AlertRuleGroupWithFolderTitle, error) { q := models.ListAlertRulesQuery{ OrgID: orgID, } + if folderUID != "" { + q.NamespaceUIDs = []string{folderUID} + } ruleList, err := service.ruleStore.ListAlertRules(ctx, &q) if err != nil {