From c006df375a3a6056f9cbeeaf2f28f87ce86823a2 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Fri, 27 Jan 2023 11:39:16 -0500 Subject: [PATCH] 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. --- .../file-provisioning/index.md | 8 +- .../http_api/alerting_provisioning.md | 851 +++++++++++------- pkg/api/response/response.go | 28 +- pkg/services/ngalert/api/api_provisioning.go | 89 ++ .../ngalert/api/api_provisioning_test.go | 478 +++++++++- pkg/services/ngalert/api/authorization.go | 5 +- .../ngalert/api/authorization_test.go | 2 +- .../api/generated_base_api_provisioning.go | 47 + pkg/services/ngalert/api/provisioning.go | 12 + pkg/services/ngalert/api/tooling/api.json | 244 ++++- .../definitions/provisioning_alert_rules.go | 55 +- pkg/services/ngalert/api/tooling/post.json | 240 ++++- pkg/services/ngalert/api/tooling/spec.json | 247 ++++- pkg/services/ngalert/ngalert.go | 2 +- .../ngalert/provisioning/alert_rules.go | 150 ++- .../alerting/{ => file}/rules_types.go | 161 +++- .../alerting/{ => file}/rules_types_test.go | 9 +- .../alerting/rules_provisioner.go | 16 +- pkg/services/provisioning/alerting/types.go | 11 +- pkg/services/provisioning/provisioning.go | 1 + public/api-merged.json | 243 ++++- 21 files changed, 2507 insertions(+), 392 deletions(-) rename pkg/services/provisioning/alerting/{ => file}/rules_types.go (50%) rename pkg/services/provisioning/alerting/{ => file}/rules_types_test.go (98%) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index 1aec13bc0e4..82b1c05ae20 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -22,20 +22,20 @@ Details on how to set up the files and which fields are required for each object **Note:** -Provisioning takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/admin/#reload-provisioning-configurations). +Provisioning takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](https://grafana.com/docs/grafana/latest/developers/http_api/admin/#reload-provisioning-configurations). ### Provision alert rules Create or delete alert rules in your Grafana instance(s). -1. Create an alert rule in Grafana. -1. Use the [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#route-get-alert-rule) to extract the alert rule. +1. Create alert rules in Grafana. +1. Use the [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#route-get-alert-rule-export) export endpoints to download a provisioning file for your alert rules. 1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory. Example configuration files can be found below. 1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s). -1. Delete the alert rule in Grafana. +1. Delete the alert rules in Grafana that will be provisioned. **Note:** diff --git a/docs/sources/developers/http_api/alerting_provisioning.md b/docs/sources/developers/http_api/alerting_provisioning.md index 1198c17eead..98ff5686667 100644 --- a/docs/sources/developers/http_api/alerting_provisioning.md +++ b/docs/sources/developers/http_api/alerting_provisioning.md @@ -18,7 +18,7 @@ title: 'Alerting Provisioning HTTP API ' ### Version -1.0.0 +1.1.0 ## Content negotiation @@ -29,53 +29,61 @@ title: 'Alerting Provisioning HTTP API ' ### Produces - application/json +- text/yaml +- application/yaml ## All endpoints ### Alert rules -| Method | URI | Name | Summary | -| ------ | ----------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------ | -| GET | /api/v1/provisioning/alert-rules/{UID} | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | -| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | -| PUT | /api/v1/provisioning/alert-rules/{UID} | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | -| PUT | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group. | -| DELETE | /api/v1/provisioning/alert-rules/{UID} | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | +| Method | URI | Name | Summary | +| ------ | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------- | +| DELETE | /api/v1/provisioning/alert-rules/{UID} | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/{UID} | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/{UID}/export | [route get alert rule export](#route-get-alert-rule-export) | Export an alert rule in provisioning file format. | +| GET | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route get alert rule group](#route-get-alert-rule-group) | Get a rule group. | +| GET | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export | [route get alert rule group export](#route-get-alert-rule-group-export) | Export an alert rule group in provisioning file format. | +| GET | /api/v1/provisioning/alert-rules | [route get alert rules](#route-get-alert-rules) | Get all the alert rules. | +| GET | /api/v1/provisioning/alert-rules/export | [route get alert rules export](#route-get-alert-rules-export) | Export all alert rules in provisioning file format. | +| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | +| PUT | /api/v1/provisioning/alert-rules/{UID} | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | +| PUT | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group. | ### Contact points | Method | URI | Name | Summary | | ------ | ----------------------------------------- | --------------------------------------------------------- | --------------------------------- | +| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | | GET | /api/v1/provisioning/contact-points | [route get contactpoints](#route-get-contactpoints) | Get all the contact points. | | POST | /api/v1/provisioning/contact-points | [route post contactpoints](#route-post-contactpoints) | Create a contact point. | | PUT | /api/v1/provisioning/contact-points/{UID} | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. | -| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | ### Notification policies -| Method | URI | Name | Summary | -| ------ | ----------------------------- | ----------------------------------------------- | ---------------------------------- | -| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | -| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | +| Method | URI | Name | Summary | +| ------ | ----------------------------- | --------------------------------------------------- | ------------------------------------ | +| DELETE | /api/v1/provisioning/policies | [route reset policy tree](#route-reset-policy-tree) | Clears the notification policy tree. | +| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | +| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | ### Mute timings | Method | URI | Name | Summary | | ------ | ---------------------------------------- | ----------------------------------------------------- | -------------------------------- | -| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | +| DELETE | /api/v1/provisioning/mute-timings/{name} | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | | GET | /api/v1/provisioning/mute-timings/{name} | [route get mute timing](#route-get-mute-timing) | Get a mute timing. | +| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | | POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. | | PUT | /api/v1/provisioning/mute-timings/{name} | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. | -| DELETE | /api/v1/provisioning/mute-timings/{name} | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | ### Templates -| Method | URI | Name | Summary | -| ------ | ------------------------------------- | ----------------------------------------------- | ------------------------------- | -| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | -| GET | /api/v1/provisioning/templates/{name} | [route get template](#route-get-template) | Get a notification template. | -| PUT | /api/v1/provisioning/templates/{name} | [route put template](#route-put-template) | Creates or updates a template. | -| DELETE | /api/v1/provisioning/templates/{name} | [route delete template](#route-delete-template) | Delete a template. | +| Method | URI | Name | Summary | +| ------ | ------------------------------------- | ----------------------------------------------- | ------------------------------------------ | +| DELETE | /api/v1/provisioning/templates/{name} | [route delete template](#route-delete-template) | Delete a template. | +| GET | /api/v1/provisioning/templates/{name} | [route get template](#route-get-template) | Get a notification template. | +| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | +| PUT | /api/v1/provisioning/templates/{name} | [route put template](#route-put-template) | Updates an existing notification template. | ## Paths @@ -87,16 +95,15 @@ DELETE /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | #### All responses -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ----------- | ---------------------------------------- | :---------: | --------------------------------------------- | -| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | -| [400](#route-delete-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-delete-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ---------- | ---------------------------------------- | :---------: | --------------------------------------------- | +| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | #### Responses @@ -106,14 +113,6 @@ Status: No Content ###### Schema -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Delete a contact point. (_RouteDeleteContactpoints_) ``` @@ -126,34 +125,23 @@ DELETE /api/v1/provisioning/contact-points/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------ | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------------ | -| [202](#route-delete-contactpoints-202) | Accepted | Ack | | [schema](#route-delete-contactpoints-202-schema) | -| [400](#route-delete-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-delete-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | ---------- | ------------------------------------------- | :---------: | ------------------------------------------------ | +| [204](#route-delete-contactpoints-204) | No Content | The contact point was deleted successfully. | | [schema](#route-delete-contactpoints-204-schema) | #### Responses -##### 202 - Ack +##### 204 - The contact point was deleted successfully. -Status: Accepted +Status: No Content -###### Schema - -[Ack](#ack) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Delete a mute timing. (_RouteDeleteMuteTiming_) @@ -163,26 +151,24 @@ DELETE /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ---------- | ----------- | :---------: | ---------------------------------------------- | -| [204](#route-delete-mute-timing-204) | No Content | Ack | | [schema](#route-delete-mute-timing-204-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ---------- | ----------------------------------------- | :---------: | ---------------------------------------------- | +| [204](#route-delete-mute-timing-204) | No Content | The mute timing was deleted successfully. | | [schema](#route-delete-mute-timing-204-schema) | #### Responses -##### 204 - Ack +##### 204 - The mute timing was deleted successfully. Status: No Content ###### Schema -[Ack](#ack) - ### Delete a template. (_RouteDeleteTemplate_) ``` @@ -197,20 +183,18 @@ DELETE /api/v1/provisioning/templates/{name} #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ---------- | ----------- | :---------: | ------------------------------------------- | -| [204](#route-delete-template-204) | No Content | Ack | | [schema](#route-delete-template-204-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ---------- | -------------------------------------- | :---------: | ------------------------------------------- | +| [204](#route-delete-template-204) | No Content | The template was deleted successfully. | | [schema](#route-delete-template-204-schema) | #### Responses -##### 204 - Ack +##### 204 - The template was deleted successfully. Status: No Content ###### Schema -[Ack](#ack) - ### Get a specific alert rule by UID. (_RouteGetAlertRule_) ``` @@ -219,34 +203,210 @@ GET /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ | -| [200](#route-get-alert-rule-200) | OK | AlertRule | | [schema](#route-get-alert-rule-200-schema) | -| [400](#route-get-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-get-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | --------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-get-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-get-alert-rule-200-schema) | +| [404](#route-get-alert-rule-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-404-schema) | #### Responses -##### 200 - AlertRule +##### 200 - ProvisionedAlertRule Status: OK ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema +###### Schema -[ValidationError](#validation-error) +### Export an alert rule in provisioning file format. (_RouteGetAlertRuleExport_) + +``` +GET /api/v1/provisioning/alert-rules/{UID}/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------- | +| [200](#route-get-alert-rule-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-export-200-schema) | +| [404](#route-get-alert-rule-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get a rule group. (_RouteGetAlertRuleGroup_) + +``` +GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | --------- | -------------- | :---------: | ------------------------------------------------ | +| [200](#route-get-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-get-alert-rule-group-200-schema) | +| [404](#route-get-alert-rule-group-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-404-schema) | + +#### Responses + +##### 200 - AlertRuleGroup + +Status: OK + +###### Schema + +[AlertRuleGroup](#alert-rule-group) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Export an alert rule group in provisioning file format. (_RouteGetAlertRuleGroupExport_) + +``` +GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------------- | +| [200](#route-get-alert-rule-group-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-group-export-200-schema) | +| [404](#route-get-alert-rule-group-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get all the alert rules. (_RouteGetAlertRules_) + +``` +GET /api/v1/provisioning/alert-rules +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | --------------------- | :---------: | ------------------------------------------- | +| [200](#route-get-alert-rules-200) | OK | ProvisionedAlertRules | | [schema](#route-get-alert-rules-200-schema) | + +#### Responses + +##### 200 - ProvisionedAlertRules + +Status: OK + +###### Schema + +[ProvisionedAlertRules](#provisioned-alert-rules) + +### Export all alert rules in provisioning file format. (_RouteGetAlertRulesExport_) + +``` +GET /api/v1/provisioning/alert-rules/export +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | ------- | --------- | :------: | ------- | -------------------------------------------------- | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- | +| [200](#route-get-alert-rules-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rules-export-200-schema) | +| [404](#route-get-alert-rules-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rules-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema ### Get all the contact points. (_RouteGetContactpoints_) @@ -254,30 +414,27 @@ Status: Bad Request GET /api/v1/provisioning/contact-points ``` +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------- | ------ | -------- | --------- | :------: | ------- | -------------- | +| name | `query` | string | `string` | | | | Filter by name | + #### All responses -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ----------- | --------------- | :---------: | --------------------------------------------- | -| [200](#route-get-contactpoints-200) | OK | Route | | [schema](#route-get-contactpoints-200-schema) | -| [400](#route-get-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-get-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ------ | ------------- | :---------: | --------------------------------------------- | +| [200](#route-get-contactpoints-200) | OK | ContactPoints | | [schema](#route-get-contactpoints-200-schema) | #### Responses -##### 200 - Route +##### 200 - ContactPoints Status: OK ###### Schema -[Route](#route) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) +[ContactPoints](#contact-points) ### Get a mute timing. (_RouteGetMuteTiming_) @@ -287,16 +444,16 @@ GET /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | ---------------- | :---------: | ------------------------------------------- | -| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | -| [400](#route-get-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timing-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | --------- | ---------------- | :---------: | ------------------------------------------- | +| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | +| [404](#route-get-mute-timing-404) | Not Found | Not found. | | [schema](#route-get-mute-timing-404-schema) | #### Responses @@ -308,13 +465,11 @@ Status: OK [MuteTimeInterval](#mute-time-interval) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Get all the mute timings. (_RouteGetMuteTimings_) @@ -324,10 +479,9 @@ GET /api/v1/provisioning/mute-timings #### All responses -| Code | Status | Description | Has headers | Schema | -| ---------------------------------- | ----------- | --------------- | :---------: | -------------------------------------------- | -| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | -| [400](#route-get-mute-timings-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timings-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ---------------------------------- | ------ | ----------- | :---------: | -------------------------------------------- | +| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | #### Responses @@ -339,14 +493,6 @@ Status: OK [MuteTimings](#mute-timings) -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Get the notification policy tree. (_RouteGetPolicyTree_) ``` @@ -355,10 +501,9 @@ GET /api/v1/provisioning/policies #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | -| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | -| [400](#route-get-policy-tree-400) | Bad Request | ValidationError | | [schema](#route-get-policy-tree-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | ----------- | :---------: | ------------------------------------------- | +| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | #### Responses @@ -370,14 +515,6 @@ Status: OK [Route](#route) -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Get a notification template. (_RouteGetTemplate_) ``` @@ -395,7 +532,7 @@ GET /api/v1/provisioning/templates/{name} | Code | Status | Description | Has headers | Schema | | ------------------------------ | --------- | -------------------- | :---------: | ---------------------------------------- | | [200](#route-get-template-200) | OK | NotificationTemplate | | [schema](#route-get-template-200-schema) | -| [404](#route-get-template-404) | Not Found | NotFound | | [schema](#route-get-template-404-schema) | +| [404](#route-get-template-404) | Not Found | Not found. | | [schema](#route-get-template-404-schema) | #### Responses @@ -405,16 +542,14 @@ Status: OK ###### Schema -[NotificationTemplate](#message-template) +[NotificationTemplate](#notification-template) -##### 404 - NotFound +##### 404 - Not found. Status: Not Found ###### Schema -[NotFound](#not-found) - ### Get all notification templates. (_RouteGetTemplates_) ``` @@ -423,28 +558,26 @@ GET /api/v1/provisioning/templates #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------- | ----------- | -------------------- | :---------: | ----------------------------------------- | -| [200](#route-get-templates-200) | OK | NotificationTemplate | | [schema](#route-get-templates-200-schema) | -| [400](#route-get-templates-400) | Bad Request | ValidationError | | [schema](#route-get-templates-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------- | --------- | --------------------- | :---------: | ----------------------------------------- | +| [200](#route-get-templates-200) | OK | NotificationTemplates | | [schema](#route-get-templates-200-schema) | +| [404](#route-get-templates-404) | Not Found | Not found. | | [schema](#route-get-templates-404-schema) | #### Responses -##### 200 - NotificationTemplate +##### 200 - NotificationTemplates Status: OK ###### Schema -[NotificationTemplate](#message-template) +[NotificationTemplates](#notification-templates) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Create a new alert rule. (_RoutePostAlertRule_) @@ -452,28 +585,33 @@ Status: Bad Request POST /api/v1/provisioning/alert-rules ``` +#### Consumes + +- application/json + #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- | -| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ----------- | +| X-Disable-Provenance | `header` | string | `string` | | | | | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | -| [201](#route-post-alert-rule-201) | Created | AlertRule | | [schema](#route-post-alert-rule-201-schema) | -| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------- | +| [201](#route-post-alert-rule-201) | Created | ProvisionedAlertRule | | [schema](#route-post-alert-rule-201-schema) | +| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | #### Responses -##### 201 - AlertRule +##### 201 - ProvisionedAlertRule Status: Created ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) ##### 400 - ValidationError @@ -501,20 +639,20 @@ POST /api/v1/provisioning/contact-points #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------------- | -| [202](#route-post-contactpoints-202) | Accepted | Ack | | [schema](#route-post-contactpoints-202-schema) | -| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------------- | +| [202](#route-post-contactpoints-202) | Accepted | EmbeddedContactPoint | | [schema](#route-post-contactpoints-202-schema) | +| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | #### Responses -##### 202 - Ack +##### 202 - EmbeddedContactPoint Status: Accepted ###### Schema -[Ack](#ack) +[EmbeddedContactPoint](#embedded-contact-point) ##### 400 - ValidationError @@ -577,27 +715,28 @@ PUT /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | -| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| X-Disable-Provenance | `header` | string | `string` | | | | | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ | -| [200](#route-put-alert-rule-200) | OK | AlertRule | | [schema](#route-put-alert-rule-200-schema) | -| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-put-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-put-alert-rule-200-schema) | +| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | #### Responses -##### 200 - AlertRule +##### 200 - ProvisionedAlertRule Status: OK ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) ##### 400 - ValidationError @@ -662,10 +801,10 @@ PUT /api/v1/provisioning/contact-points/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier | -| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------ | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | +| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | #### All responses @@ -704,10 +843,10 @@ PUT /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | -| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | +| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | #### All responses @@ -746,9 +885,9 @@ PUT /api/v1/provisioning/policies #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ----------- | -| Body | `body` | [Route](#route) | `models.Route` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ---------------------------------------- | +| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use | #### All responses @@ -775,7 +914,7 @@ Status: Bad Request [ValidationError](#validation-error) -### Updates an existing template. (_RoutePutTemplate_) +### Updates an existing notification template. (_RoutePutTemplate_) ``` PUT /api/v1/provisioning/templates/{name} @@ -787,27 +926,27 @@ PUT /api/v1/provisioning/templates/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | -------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | -| Body | `body` | [NotificationTemplateContent](#message-template-content) | `models.NotificationTemplateContent` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- | +| name | `path` | string | `string` | | ✓ | | Template Name | +| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------- | -| [202](#route-put-template-202) | Accepted | Ack | | [schema](#route-put-template-202-schema) | -| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------- | +| [202](#route-put-template-202) | Accepted | NotificationTemplate | | [schema](#route-put-template-202-schema) | +| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | #### Responses -##### 202 - Ack +##### 202 - NotificationTemplate Status: Accepted ###### Schema -[Ack](#ack) +[NotificationTemplate](#notification-template) ##### 400 - ValidationError @@ -817,57 +956,116 @@ Status: Bad Request [ValidationError](#validation-error) +### Clears the notification policy tree. (_RouteResetPolicyTree_) + +``` +DELETE /api/v1/provisioning/policies +``` + +#### Consumes + +- application/json + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | -------- | ----------- | :---------: | --------------------------------------------- | +| [202](#route-reset-policy-tree-202) | Accepted | Ack | | [schema](#route-reset-policy-tree-202-schema) | + +#### Responses + +##### 202 - Ack + +Status: Accepted + +###### Schema + +[Ack](#ack) + +## Models + +### Ack + +[interface{}](#interface) + ### AlertQuery **Properties** | Name | Type | Go type | Required | Default | Description | Example | | --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | -------------------------------------------------------------------------------------------------- | ------- | -| DatasourceUID | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | | -| Model | object | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | -| QueryType | string | `string` | | | QueryType is an optional identifier for the type of query. | +| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | | +| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | +| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. | | It can be used to distinguish different types of queries. | | -| RefID | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | +| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | | relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | -### AlertRule +### AlertQueryExport **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | -| Condition | string | `string` | ✓ | | | `A` | -| Data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | -| ExecErrState | string | `string` | ✓ | | Allowed values: "OK", "Alerting", "Error" | | -| FolderUID | string | `string` | ✓ | | | `project_x` | -| ID | int64 (formatted integer) | `int64` | | | | | -| Labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | -| NoDataState | string | `string` | ✓ | | Allowed values: "OK", "NoData", "Error" | | -| OrgID | int64 (formatted integer) | `int64` | ✓ | | | | -| RuleGroup | string | `string` | ✓ | | | `eval_group_1` | -| Title | string | `string` | ✓ | | | `Always firing` | -| UID | string | `string` | | | | | -| Updated | date-time (formatted string) | `strfmt.DateTime` | | | | | -| for | [Duration](#duration) | `Duration` | ✓ | | | | -| provenance | string | `Provenance` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ----------------- | ----------------------------------------- | ------------------- | :------: | ------- | ----------- | ------- | +| datasourceUid | string | `string` | | | | | +| model | [interface{}](#interface) | `interface{}` | | | | | +| queryType | string | `string` | | | | | +| refId | string | `string` | | | | | +| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | + +### AlertRuleExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ----------------------------------------- | --------------------- | :------: | ------- | ----------- | ------- | +| annotations | map of string | `map[string]string` | | | | | +| condition | string | `string` | | | | | +| dasboardUid | string | `string` | | | | | +| data | [][alertqueryexport](#alert-query-export) | `[]*AlertQueryExport` | | | | | +| execErrState | string | `string` | | | | | +| for | [Duration](#duration) | `Duration` | | | | | +| labels | map of string | `map[string]string` | | | | | +| noDataState | string | `string` | | | | | +| panelId | int64 (formatted integer) | `int64` | | | | | +| title | string | `string` | | | | | +| uid | string | `string` | | | | | ### AlertRuleGroup **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Interval | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| --------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | +| folderUid | string | `string` | | | | | +| interval | int64 (formatted integer) | `int64` | | | | | +| rules | [][provisionedalertrule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | | +| title | string | `string` | | | | | -### DayOfMonthRange +### AlertRuleGroupExport **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | --------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | +| folder | string | `string` | | | | | +| interval | [Duration](#duration) | `Duration` | | | | | +| name | string | `string` | | | | | +| orgId | int64 (formatted integer) | `int64` | | | | | +| rules | [][alertruleexport](#alert-rule-export) | `[]*AlertRuleExport` | | | | | + +### AlertingFileExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ---------- | -------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | +| apiVersion | int64 (formatted integer) | `int64` | | | | | +| groups | [][alertrulegroupexport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | | + +### ContactPoints + +[][embeddedcontactpoint](#embedded-contact-point) ### Duration @@ -877,25 +1075,35 @@ Status: Bad Request ### EmbeddedContactPoint -> EmbeddedContactPoint is the contact point integration that is used +> EmbeddedContactPoint is the contact point type that is used > by grafanas embedded alertmanager implementation. **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| --------------------- | ------- | -------- | :------: | ------- | ---------------------------------------------------------------------------------------------------- | ----------------------- | -| DisableResolveMessage | boolean | `bool` | | | | `false` | -| Name | string | `string` | ✓ | | Name is used as grouping key in the UI. Contact points with the same name will be grouped in the UI. | `webhook_1` | -| Provenance | string | `string` | | | | | -| Type | string | `string` | ✓ | | | `webhook` | -| UID | string | `string` | | | UID is the unique identifier of the contact point. The UID can be set by the user. | `my_external_reference` | -| settings | object | `JSON` | ✓ | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------------------------ | ----------------------- | -------- | :------: | ------- | ----------------------------------------------------------------- | --------- | +| disableResolveMessage | boolean | `bool` | | | | `false` | +| name | string | `string` | | | Name is used as grouping key in the UI. Contact points with the | +| same name will be grouped in the UI. | `webhook_1` | +| provenance | string | `string` | | | | | +| settings | [JSON](#json) | `JSON` | ✓ | | | | +| type | string | `string` | ✓ | | | `webhook` | +| uid | string | `string` | | | UID is the unique identifier of the contact point. The UID can be | +| set by the user. | `my_external_reference` | + +### Json + +[interface{}](#interface) + +### MatchRegexps + +[MatchRegexps](#match-regexps) ### MatchType -| Name | Type | Go type | Default | Description | Example | -| --------- | ------------------------- | ------- | ------- | ---------------------------------------------------------------------- | ------- | -| MatchType | int64 (formatted integer) | int64 | | 0 = MatchEqual, 1 = MatchNotEqual, 2 = MatchRegexp, 3 = MatchNotRegexp | | +| Name | Type | Go type | Default | Description | Example | +| --------- | ------------------------- | ------- | ------- | ----------- | ------- | +| MatchType | int64 (formatted integer) | int64 | | | | ### Matcher @@ -915,49 +1123,40 @@ Status: Bad Request [][matcher](#matcher) -### NotificationTemplate - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ---------- | ------ | ------------ | :------: | ------- | ----------- | ------- | -| Name | string | `string` | | | | | -| Template | string | `string` | | | | | -| provenance | string | `Provenance` | | | | | - -### NotificationTemplateContent - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | -| Template | string | `string` | | | | | - -### MonthRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | - ### MuteTimeInterval **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | -| Name | string | `string` | | | | | -| TimeIntervals | [][timeinterval](#time-interval) | `[]*TimeInterval` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| -------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| time_intervals | [][timeinterval](#time-interval) | `[]*TimeInterval` | | | | | ### MuteTimings [][mutetimeinterval](#mute-time-interval) -### NotFound +### NotificationTemplate -[interface{}](#interface) +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ---------- | ------------------------- | ------------ | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| template | string | `string` | | | | | + +### NotificationTemplateContent + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | +| template | string | `string` | | | | | + +### NotificationTemplates + +[][notificationtemplate](#notification-template) ### ObjectMatchers @@ -965,6 +1164,45 @@ Status: Bad Request #### Inlined models +### Provenance + +| Name | Type | Go type | Default | Description | Example | +| ---------- | ------ | ------- | ------- | ----------- | ------- | +| Provenance | string | string | | | | + +### ProvisionedAlertRule + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | +| condition | string | `string` | ✓ | | | `A` | +| data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | +| execErrState | string | `string` | ✓ | | | | +| folderUID | string | `string` | ✓ | | | `project_x` | +| for | [Duration](#duration) | `Duration` | ✓ | | | | +| id | int64 (formatted integer) | `int64` | | | | | +| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | +| noDataState | string | `string` | ✓ | | | | +| orgID | int64 (formatted integer) | `int64` | ✓ | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| ruleGroup | string | `string` | ✓ | | | `eval_group_1` | +| title | string | `string` | ✓ | | | `Always firing` | +| uid | string | `string` | | | | | +| updated | date-time (formatted string) | `strfmt.DateTime` | | | | | + +### ProvisionedAlertRules + +[][provisionedalertrule](#provisioned-alert-rule) + +### Regexp + +> A Regexp is safe for concurrent use by multiple goroutines, +> except for configuration methods, such as Longest. + +[interface{}](#interface) + ### RelativeTimeRange > RelativeTimeRange is the per query start and end time @@ -979,22 +1217,26 @@ Status: Bad Request ### Route -> A Route is a node that contains definitions of how to handle alerts. +> A Route is a node that contains definitions of how to handle alerts. This is modified +> from the upstream alertmanager in that it adds the ObjectMatchers property. **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----------------- | ---------------------------------- | ---------------- | :------: | ------- | ----------- | ------- | -| Continue | boolean | `bool` | | | | | -| GroupByStr | []string | `[]string` | | | | | -| MuteTimeIntervals | []string | `[]string` | | | | | -| Receiver | string | `string` | | | | | -| Routes | [][route](#route) | `[]*Route` | | | | | -| group_interval | [Duration](#duration) | `Duration` | | | | | -| group_wait | [Duration](#duration) | `Duration` | | | | | -| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | -| provenance | string | `Provenance` | | | | | -| repeat_interval | [Duration](#duration) | `Duration` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- | +| continue | boolean | `bool` | | | | | +| group_by | []string | `[]string` | | | | | +| group_interval | string | `string` | | | | | +| group_wait | string | `string` | | | | | +| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | | +| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | | +| matchers | [Matchers](#matchers) | `Matchers` | | | | | +| mute_time_intervals | []string | `[]string` | | | | | +| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| receiver | string | `string` | | | | | +| repeat_interval | string | `string` | | | | | +| routes | [][route](#route) | `[]*Route` | | | | | ### TimeInterval @@ -1003,13 +1245,14 @@ Status: Bad Request **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----------- | ---------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | -| DaysOfMonth | [][dayofmonthrange](#day-of-month-range) | `[]*DayOfMonthRange` | | | | | -| Months | [][monthrange](#month-range) | `[]*MonthRange` | | | | | -| Times | [][timerange](#time-range) | `[]*TimeRange` | | | | | -| Weekdays | [][weekdayrange](#weekday-range) | `[]*WeekdayRange` | | | | | -| Years | [][yearrange](#year-range) | `[]*YearRange` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------- | -------------------------- | -------------- | :------: | ------- | ----------- | ------- | +| days_of_month | []string | `[]string` | | | | | +| location | string | `string` | | | | | +| months | []string | `[]string` | | | | | +| times | [][timerange](#time-range) | `[]*TimeRange` | | | | | +| weekdays | []string | `[]string` | | | | | +| years | []string | `[]string` | | | | | ### TimeRange @@ -1026,24 +1269,6 @@ Status: Bad Request **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ---- | ------ | -------- | :------: | ------- | ----------- | ------- | -| Msg | string | `string` | | | | | - -### WeekdayRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | - -### YearRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ---- | ------ | -------- | :------: | ------- | ----------- | --------------- | +| msg | string | `string` | | | | `error message` | diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index d131bff1caa..aa8839d169f 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -9,6 +9,7 @@ import ( "reflect" jsoniter "github.com/json-iterator/go" + "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/tracing" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -173,7 +174,8 @@ func (r *RedirectResponse) Body() []byte { // JSON creates a JSON response. func JSON(status int, body interface{}) *NormalResponse { - return Respond(status, body).SetHeader("Content-Type", "application/json") + return Respond(status, body). + SetHeader("Content-Type", "application/json") } // JSONStreaming creates a streaming JSON response. @@ -187,6 +189,30 @@ func JSONStreaming(status int, body interface{}) StreamingResponse { } } +// JSONDownload creates a JSON response indicating that it should be downloaded. +func JSONDownload(status int, body interface{}, filename string) *NormalResponse { + return JSON(status, body). + SetHeader("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, filename)) +} + +// YAML creates a YAML response. +func YAML(status int, body interface{}) *NormalResponse { + b, err := yaml.Marshal(body) + if err != nil { + return Error(http.StatusInternalServerError, "body yaml marshal", err) + } + // As of now, application/yaml is downloaded by default in chrome regardless of Content-Disposition, so we use text/yaml instead. + return Respond(status, b). + SetHeader("Content-Type", "text/yaml") +} + +// YAMLDownload creates a YAML response indicating that it should be downloaded. +func YAMLDownload(status int, body interface{}, filename string) *NormalResponse { + return YAML(status, body). + SetHeader("Content-Type", "application/yaml"). + SetHeader("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, filename)) +} + // Success create a successful response func Success(message string) *NormalResponse { resp := make(map[string]interface{}) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 35f2e8c99bf..93c3e3696bf 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -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) +} diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 3408e0a34c5..183bf0c8d91 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -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": { diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 4dfc8a85ab3..0a6c2a4d005 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -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 diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index b4da61566fd..2957979a26d 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -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} diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index aa72ebe5efa..f3e6ed66c32 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -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"), diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go index 23387e36c11..d26b6bb5e75 100644 --- a/pkg/services/ngalert/api/provisioning.go +++ b/pkg/services/ngalert/api/provisioning.go @@ -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) } diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 86cd1cc9299..5c39db61c9e 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -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", 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 8bf8c656a5c..0e51a514bbf 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -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, diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index ce003de9465..b30fb5961d6 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -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", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index ce7a76b2117..fcae8f39245 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -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", diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index cc92ef97891..3908c308cd0 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -239,7 +239,7 @@ func (ng *AlertNG) init() error { contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) templateService := provisioning.NewTemplateService(store, store, store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) - alertRuleService := provisioning.NewAlertRuleService(store, store, ng.QuotaService, store, + alertRuleService := provisioning.NewAlertRuleService(store, store, ng.dashboardService, ng.QuotaService, store, int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()), ng.Log) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 27d6b531946..18b2ba3f361 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -4,11 +4,14 @@ import ( "context" "errors" "fmt" + "sort" "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/util" ) @@ -18,6 +21,7 @@ type AlertRuleService struct { baseIntervalSeconds int64 ruleStore RuleStore provenanceStore ProvisioningStore + dashboardService dashboards.DashboardService quotas QuotaChecker xact TransactionManager log log.Logger @@ -25,6 +29,7 @@ type AlertRuleService struct { func NewAlertRuleService(ruleStore RuleStore, provenanceStore ProvisioningStore, + dashboardService dashboards.DashboardService, quotas QuotaChecker, xact TransactionManager, defaultIntervalSeconds int64, @@ -35,6 +40,7 @@ func NewAlertRuleService(ruleStore RuleStore, baseIntervalSeconds: baseIntervalSeconds, ruleStore: ruleStore, provenanceStore: provenanceStore, + dashboardService: dashboardService, quotas: quotas, xact: xact, log: log, @@ -69,6 +75,38 @@ func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64, return *query.Result, provenance, nil } +type AlertRuleWithFolderTitle struct { + AlertRule models.AlertRule + FolderTitle string +} + +// GetAlertRuleWithFolderTitle returns a single alert rule with its folder title. +func (service *AlertRuleService) GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (AlertRuleWithFolderTitle, error) { + query := &models.GetAlertRuleByUIDQuery{ + OrgID: orgID, + UID: ruleUID, + } + err := service.ruleStore.GetAlertRuleByUID(ctx, query) + if err != nil { + return AlertRuleWithFolderTitle{}, err + } + + dq := dashboards.GetDashboardQuery{ + OrgID: orgID, + UID: query.Result.NamespaceUID, + } + + dash, err := service.dashboardService.GetDashboard(ctx, &dq) + if err != nil { + return AlertRuleWithFolderTitle{}, err + } + + return AlertRuleWithFolderTitle{ + AlertRule: *query.Result, + FolderTitle: dash.Title, + }, nil +} + // CreateAlertRule creates a new alert rule. This function will ignore any // interval that is set in the rule struct and use the already existing group // interval or the default one. @@ -114,10 +152,10 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule model return rule, nil } -func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (models.AlertRuleGroup, error) { +func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64, namespaceUID, group string) (models.AlertRuleGroup, error) { q := models.ListAlertRulesQuery{ OrgID: orgID, - NamespaceUIDs: []string{folder}, + NamespaceUIDs: []string{namespaceUID}, RuleGroup: group, } if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { @@ -366,6 +404,114 @@ func (service *AlertRuleService) deleteRules(ctx context.Context, orgID int64, t return nil } +// GetAlertRuleGroupWithFolderTitle returns the alert rule group with folder title. +func (service *AlertRuleService) GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, namespaceUID, group string) (file.AlertRuleGroupWithFolderTitle, error) { + q := models.ListAlertRulesQuery{ + OrgID: orgID, + NamespaceUIDs: []string{namespaceUID}, + RuleGroup: group, + } + if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { + return file.AlertRuleGroupWithFolderTitle{}, err + } + if len(q.Result) == 0 { + return file.AlertRuleGroupWithFolderTitle{}, store.ErrAlertRuleGroupNotFound + } + + dq := dashboards.GetDashboardQuery{ + OrgID: orgID, + UID: namespaceUID, + } + dash, err := service.dashboardService.GetDashboard(ctx, &dq) + if err != nil { + return file.AlertRuleGroupWithFolderTitle{}, err + } + + res := file.AlertRuleGroupWithFolderTitle{ + AlertRuleGroup: &models.AlertRuleGroup{ + Title: q.Result[0].RuleGroup, + FolderUID: q.Result[0].NamespaceUID, + Interval: q.Result[0].IntervalSeconds, + Rules: []models.AlertRule{}, + }, + OrgID: orgID, + FolderTitle: dash.Title, + } + for _, r := range q.Result { + if r != nil { + res.AlertRuleGroup.Rules = append(res.AlertRuleGroup.Rules, *r) + } + } + 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) ([]file.AlertRuleGroupWithFolderTitle, error) { + q := models.ListAlertRulesQuery{ + OrgID: orgID, + } + + if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { + return nil, err + } + + groups := make(map[models.AlertRuleGroupKey][]models.AlertRule) + namespaces := make(map[string][]*models.AlertRuleGroupKey) + for _, r := range q.Result { + groupKey := r.GetGroupKey() + group := groups[groupKey] + group = append(group, *r) + groups[groupKey] = group + + namespaces[r.NamespaceUID] = append(namespaces[r.NamespaceUID], &groupKey) + } + + dq := dashboards.GetDashboardsQuery{ + DashboardUIDs: nil, + } + for uid := range namespaces { + dq.DashboardUIDs = append(dq.DashboardUIDs, uid) + } + + // We need folder titles for the provisioning file format. We do it this way instead of using GetUserVisibleNamespaces to avoid folder:read permissions that should not apply to those with alert.provisioning:read. + dashes, err := service.dashboardService.GetDashboards(ctx, &dq) + if err != nil { + return nil, err + } + folderUidToTitle := make(map[string]string) + for _, dash := range dashes { + folderUidToTitle[dash.UID] = dash.Title + } + + result := make([]file.AlertRuleGroupWithFolderTitle, 0) + for groupKey, rules := range groups { + title, ok := folderUidToTitle[groupKey.NamespaceUID] + if !ok { + return nil, fmt.Errorf("cannot find title for folder with uid '%s'", groupKey.NamespaceUID) + } + result = append(result, file.AlertRuleGroupWithFolderTitle{ + AlertRuleGroup: &models.AlertRuleGroup{ + Title: rules[0].RuleGroup, + FolderUID: rules[0].NamespaceUID, + Interval: rules[0].IntervalSeconds, + Rules: rules, + }, + OrgID: orgID, + FolderTitle: title, + }) + } + + // Return results in a stable manner. + sort.SliceStable(result, func(i, j int) bool { + if result[i].AlertRuleGroup.FolderUID == result[j].AlertRuleGroup.FolderUID { + return result[i].AlertRuleGroup.Title < result[j].AlertRuleGroup.Title + } + return result[i].AlertRuleGroup.FolderUID < result[j].AlertRuleGroup.FolderUID + }) + + return result, nil +} + // syncRuleGroupFields synchronizes calculated fields across multiple rules in a group. func syncGroupRuleFields(group *models.AlertRuleGroup, orgID int64) *models.AlertRuleGroup { for i := range group.Rules { diff --git a/pkg/services/provisioning/alerting/rules_types.go b/pkg/services/provisioning/alerting/file/rules_types.go similarity index 50% rename from pkg/services/provisioning/alerting/rules_types.go rename to pkg/services/provisioning/alerting/file/rules_types.go index b300d918c46..3c726dffc41 100644 --- a/pkg/services/provisioning/alerting/rules_types.go +++ b/pkg/services/provisioning/alerting/file/rules_types.go @@ -1,4 +1,4 @@ -package alerting +package file import ( "encoding/json" @@ -31,11 +31,11 @@ type AlertRuleGroupV1 struct { Rules []AlertRuleV1 `json:"rules" yaml:"rules"` } -func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) { - ruleGroup := AlertRuleGroup{} - ruleGroup.Name = ruleGroupV1.Name.Value() - if strings.TrimSpace(ruleGroup.Name) == "" { - return AlertRuleGroup{}, errors.New("rule group has no name set") +func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroupWithFolderTitle, error) { + ruleGroup := AlertRuleGroupWithFolderTitle{AlertRuleGroup: &models.AlertRuleGroup{}} + ruleGroup.Title = ruleGroupV1.Name.Value() + if strings.TrimSpace(ruleGroup.Title) == "" { + return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no name set") } ruleGroup.OrgID = ruleGroupV1.OrgID.Value() if ruleGroup.OrgID < 1 { @@ -43,29 +43,27 @@ func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) { } interval, err := model.ParseDuration(ruleGroupV1.Interval.Value()) if err != nil { - return AlertRuleGroup{}, err + return AlertRuleGroupWithFolderTitle{}, err } - ruleGroup.Interval = time.Duration(interval) - ruleGroup.Folder = ruleGroupV1.Folder.Value() - if strings.TrimSpace(ruleGroup.Folder) == "" { - return AlertRuleGroup{}, errors.New("rule group has no folder set") + ruleGroup.Interval = int64(time.Duration(interval).Seconds()) + ruleGroup.FolderTitle = ruleGroupV1.Folder.Value() + if strings.TrimSpace(ruleGroup.FolderTitle) == "" { + return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no folder set") } for _, ruleV1 := range ruleGroupV1.Rules { rule, err := ruleV1.mapToModel(ruleGroup.OrgID) if err != nil { - return AlertRuleGroup{}, err + return AlertRuleGroupWithFolderTitle{}, err } ruleGroup.Rules = append(ruleGroup.Rules, rule) } return ruleGroup, nil } -type AlertRuleGroup struct { - OrgID int64 - Name string - Folder string - Interval time.Duration - Rules []models.AlertRule +type AlertRuleGroupWithFolderTitle struct { + *models.AlertRuleGroup + OrgID int64 + FolderTitle string } type AlertRuleV1 struct { @@ -175,3 +173,130 @@ func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) { Model: rawMessage, }, nil } + +// Response structs + +// AlertingFileExport is the full provisioned file export. +// swagger:model +type AlertingFileExport struct { + APIVersion int64 `json:"apiVersion" yaml:"apiVersion"` + Groups []AlertRuleGroupExport `json:"groups" yaml:"groups"` +} + +// AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1. +type AlertRuleGroupExport struct { + OrgID int64 `json:"orgId" yaml:"orgId"` + Name string `json:"name" yaml:"name"` + Folder string `json:"folder" yaml:"folder"` + Interval model.Duration `json:"interval" yaml:"interval"` + Rules []AlertRuleExport `json:"rules" yaml:"rules"` +} + +// AlertRuleExport is the provisioned file export of models.AlertRule. +type AlertRuleExport struct { + UID string `json:"uid" yaml:"uid"` + Title string `json:"title" yaml:"title"` + Condition string `json:"condition" yaml:"condition"` + Data []AlertQueryExport `json:"data" yaml:"data"` + DashboardUID string `json:"dasboardUid,omitempty" yaml:"dashboardUid,omitempty"` + PanelID int64 `json:"panelId,omitempty" yaml:"panelId,omitempty"` + NoDataState models.NoDataState `json:"noDataState" yaml:"noDataState"` + ExecErrState models.ExecutionErrorState `json:"execErrState" yaml:"execErrState"` + For model.Duration `json:"for" yaml:"for"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` +} + +// AlertQueryExport is the provisioned export of models.AlertQuery. +type AlertQueryExport struct { + RefID string `json:"refId" yaml:"refId"` + QueryType string `json:"queryType,omitempty" yaml:"queryType,omitempty"` + RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange,omitempty" yaml:"relativeTimeRange,omitempty"` + DatasourceUID string `json:"datasourceUid" yaml:"datasourceUid"` + Model map[string]interface{} `json:"model" yaml:"model"` +} + +// NewAlertingFileExport creates an AlertingFileExport DTO from []AlertRuleGroupWithFolderTitle. +func NewAlertingFileExport(groups []AlertRuleGroupWithFolderTitle) (AlertingFileExport, error) { + f := AlertingFileExport{APIVersion: 1} + for _, group := range groups { + export, err := newAlertRuleGroupExport(group) + if err != nil { + return AlertingFileExport{}, err + } + f.Groups = append(f.Groups, export) + } + return f, nil +} + +// newAlertRuleGroupExport creates a AlertRuleGroupExport DTO from models.AlertRuleGroup. +func newAlertRuleGroupExport(d AlertRuleGroupWithFolderTitle) (AlertRuleGroupExport, error) { + rules := make([]AlertRuleExport, 0, len(d.Rules)) + for i := range d.Rules { + alert, err := newAlertRuleExport(d.Rules[i]) + if err != nil { + return AlertRuleGroupExport{}, err + } + rules = append(rules, alert) + } + return AlertRuleGroupExport{ + OrgID: d.OrgID, + Name: d.Title, + Folder: d.FolderTitle, + Interval: model.Duration(time.Duration(d.Interval) * time.Second), + Rules: rules, + }, nil +} + +// newAlertRuleExport creates a AlertRuleExport DTO from models.AlertRule. +func newAlertRuleExport(rule models.AlertRule) (AlertRuleExport, error) { + data := make([]AlertQueryExport, 0, len(rule.Data)) + for i := range rule.Data { + query, err := newAlertQueryExport(rule.Data[i]) + if err != nil { + return AlertRuleExport{}, err + } + data = append(data, query) + } + + var dashboardUID string + if rule.DashboardUID != nil { + dashboardUID = *rule.DashboardUID + } + + var panelID int64 + if rule.PanelID != nil { + panelID = *rule.PanelID + } + + return AlertRuleExport{ + UID: rule.UID, + Title: rule.Title, + For: model.Duration(rule.For), + Condition: rule.Condition, + Data: data, + DashboardUID: dashboardUID, + PanelID: panelID, + NoDataState: rule.NoDataState, + ExecErrState: rule.ExecErrState, + Annotations: rule.Annotations, + Labels: rule.Labels, + }, nil +} + +// newAlertQueryExport creates a AlertQueryExport DTO from models.AlertQuery. +func newAlertQueryExport(query models.AlertQuery) (AlertQueryExport, error) { + // We unmarshal the json.RawMessage model into a map in order to facilitate yaml marshalling. + var mdl map[string]interface{} + err := json.Unmarshal(query.Model, &mdl) + if err != nil { + return AlertQueryExport{}, err + } + return AlertQueryExport{ + RefID: query.RefID, + QueryType: query.QueryType, + RelativeTimeRange: query.RelativeTimeRange, + DatasourceUID: query.DatasourceUID, + Model: mdl, + }, nil +} diff --git a/pkg/services/provisioning/alerting/rules_types_test.go b/pkg/services/provisioning/alerting/file/rules_types_test.go similarity index 98% rename from pkg/services/provisioning/alerting/rules_types_test.go rename to pkg/services/provisioning/alerting/file/rules_types_test.go index 0eac93e2404..abc2836f01a 100644 --- a/pkg/services/provisioning/alerting/rules_types_test.go +++ b/pkg/services/provisioning/alerting/file/rules_types_test.go @@ -1,13 +1,14 @@ -package alerting +package file import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/provisioning/values" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/provisioning/values" ) func TestRuleGroup(t *testing.T) { @@ -60,7 +61,7 @@ func TestRuleGroup(t *testing.T) { rg.Interval = interval rgMapped, err := rg.MapToModel() require.NoError(t, err) - require.Equal(t, 48*time.Hour, rgMapped.Interval) + require.Equal(t, int64(48*time.Hour/time.Second), rgMapped.Interval) }) t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) { rg := validRuleGroupV1(t) diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index 55750ff0421..36fdbef76ea 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -41,24 +41,24 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context, files []*AlertingFile) error { for _, file := range files { for _, group := range file.Groups { - folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID) + folderUID, err := prov.getOrCreateFolderUID(ctx, group.FolderTitle, group.OrgID) if err != nil { return err } prov.logger.Debug("provisioning alert rule group", "org", group.OrgID, - "folder", group.Folder, + "folder", group.FolderTitle, "folderUID", folderUID, - "name", group.Name) + "name", group.Title) for _, rule := range group.Rules { rule.NamespaceUID = folderUID - rule.RuleGroup = group.Name - err = prov.provisionRule(ctx, group.OrgID, rule, group.Folder, folderUID) + rule.RuleGroup = group.Title + err = prov.provisionRule(ctx, group.OrgID, rule) if err != nil { return err } } - err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Name, int64(group.Interval.Seconds())) + err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Title, group.Interval) if err != nil { return err } @@ -77,9 +77,7 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context, func (prov *defaultAlertRuleProvisioner) provisionRule( ctx context.Context, orgID int64, - rule alert_models.AlertRule, - folder, - folderUID string) error { + rule alert_models.AlertRule) error { prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID) _, _, err := prov.ruleService.GetAlertRule(ctx, orgID, rule.UID) if err != nil && !errors.Is(err, alert_models.ErrAlertRuleNotFound) { diff --git a/pkg/services/provisioning/alerting/types.go b/pkg/services/provisioning/alerting/types.go index 6b500c85dd1..6a3331c095b 100644 --- a/pkg/services/provisioning/alerting/types.go +++ b/pkg/services/provisioning/alerting/types.go @@ -3,6 +3,7 @@ package alerting import ( "fmt" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/services/provisioning/values" ) @@ -15,8 +16,8 @@ type OrgID int64 type AlertingFile struct { configVersion Filename string - Groups []AlertRuleGroup - DeleteRules []RuleDelete + Groups []file.AlertRuleGroupWithFolderTitle + DeleteRules []file.RuleDelete ContactPoints []ContactPoint DeleteContactPoints []DeleteContactPoint Policies []NotificiationPolicy @@ -30,8 +31,8 @@ type AlertingFile struct { type AlertingFileV1 struct { configVersion Filename string - Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"` - DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` + Groups []file.AlertRuleGroupV1 `json:"groups" yaml:"groups"` + DeleteRules []file.RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"` DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"` Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"` @@ -132,7 +133,7 @@ func (fileV1 *AlertingFileV1) mapRules(alertingFile *AlertingFile) error { if orgID < 1 { orgID = 1 } - ruleDelete := RuleDelete{ + ruleDelete := file.RuleDelete{ UID: ruleDeleteV1.UID.Value(), OrgID: orgID, } diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index fe6f7c727fa..9f7bff61917 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -269,6 +269,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error ruleService := provisioning.NewAlertRuleService( st, st, + ps.dashboardService, ps.quotaService, ps.SQLStore, int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), diff --git a/public/api-merged.json b/public/api-merged.json index 8f18508d55e..2505ee36660 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2496,6 +2496,35 @@ } } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "tags": [ + "provisioning" + ], + "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": [ @@ -2591,6 +2620,47 @@ } } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning" + ], + "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": [ @@ -2794,6 +2864,52 @@ } } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning" + ], + "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": [ @@ -11044,6 +11160,28 @@ } } }, + "AlertQueryExport": { + "type": "object", + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "type": "object", + "additionalProperties": false + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + } + }, "AlertResponse": { "type": "object", "required": [ @@ -11064,6 +11202,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": { @@ -11085,6 +11282,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": { @@ -11174,6 +11396,22 @@ } } }, + "AlertingFileExport": { + "type": "object", + "title": "AlertingFileExport is the full provisioned file export.", + "properties": { + "apiVersion": { + "type": "integer", + "format": "int64" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + } + } + } + }, "AlertingRule": { "description": "adapted from cortex", "type": "object", @@ -17824,9 +18062,8 @@ "type": "string" }, "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.", "type": "object", - "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.", "properties": { "ForceQuery": { "type": "boolean" @@ -18852,6 +19089,7 @@ } }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -18912,7 +19150,6 @@ } }, "integration": { - "description": "Integration integration", "type": "object", "required": [ "name",