Alerting: Add notification policy provisioning file export (#70009)

* Alerting: Add notification policy provisioning file export

- Add provisioning API endpoint for exporting notification policies.
- Add option in notification policy view ellipsis dropdown for exporting.
- Update various provisioning documentation.
This commit is contained in:
Matthew Jacobson 2023-07-24 17:56:53 -04:00 committed by GitHub
parent 4c42632ab8
commit cfb1656968
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 731 additions and 71 deletions

View File

@ -496,11 +496,13 @@ settings:
Create or reset the notification policy tree in your Grafana instance(s).
1. Create a YAML or JSON configuration file.
1. Create a notification policy in Grafana.
2. Use the [Alerting provisioning API]({{< relref "../../../../developers/http_api/alerting_provisioning" >}}) export endpoints to download a provisioning file for your notification policy.
3. 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.
2. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
4. 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).
Here is an example of a configuration file for creating notification policies.

View File

@ -70,11 +70,12 @@ Contact point provisioning is for Grafana-managed alerts only.
### Notification policies
| 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. |
| 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. |
| GET | /api/v1/provisioning/policies/export | [route get policy tree export](#route-get-policy-tree-export) | Export the notification policy tree in provisioning file format. |
| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. |
### Mute timings
@ -573,6 +574,37 @@ Status: OK
[Route](#route)
### <span id="route-get-policy-tree-export"></span> Export the notification policy tree in provisioning file format. (_RouteGetPolicyTreeExport_)
```
GET /api/v1/provisioning/policies/export
```
#### All responses
| Code | Status | Description | Has headers | Schema |
| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- |
| [200](#route-get-policy-tree-export-200) | OK | AlertingFileExport | | [schema](#route-get-policy-tree-export-200-schema) |
| [404](#route-get-policy-tree-export-404) | Not Found | NotFound | | [schema](#route-get-policy-tree-export-404-schema) |
#### Responses
##### <span id="route-get-policy-tree-export-200"></span> 200 - AlertingFileExport
Status: OK
###### <span id="route-get-policy-tree-export-200-schema"></span> Schema
[AlertingFileExport](#alerting-file-export)
##### <span id="route-get-policy-tree-export-404"></span> 404 - NotFound
Status: Not Found
###### <span id="route-get-policy-tree-export-404-schema"></span> Schema
[NotFound](#not-found)
### <span id="route-get-template"></span> Get a notification template. (_RouteGetTemplate_)
```
@ -1182,11 +1214,12 @@ Status: Accepted
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ------------- | -------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- |
| apiVersion | int64 (formatted integer) | `int64` | | | | |
| contactPoints | [][ContactPointExport](#contact-point-export) | `[]*ContactPointExport` | | | | |
| groups | [][AlertRuleGroupExport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------------- | --------------------------------------------------------- | ----------------------------- | :------: | ------- | ----------- | ------- |
| apiVersion | int64 (formatted integer) | `int64` | | | | |
| contactPoints | [][ContactPointExport](#contact-point-export) | `[]*ContactPointExport` | | | | |
| groups | [][AlertRuleGroupExport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | |
| policies | [][NotificationPolicyExport](#notification-policy-export) | `[]*NotificationPolicyExport` | | | | |
{{% /responsive-table %}}
@ -1285,6 +1318,19 @@ Status: Accepted
[][MuteTimeInterval](#mute-time-interval)
### <span id="not-found"></span> NotFound
[interface{}](#interface)
### <span id="notification-policy-export"></span> NotificationPolicyExport
**Properties**
| Name | Type | Go type | Required | Default | Description | Example |
| ------ | ---------------------------- | ------------- | :------: | ------- | ----------- | ------- |
| Policy | [RouteExport](#route-export) | `RouteExport` | | | inline | |
| orgId | int64 (formatted integer) | `int64` | | | | |
### <span id="notification-template"></span> NotificationTemplate
**Properties**
@ -1362,16 +1408,20 @@ Status: Accepted
[][ProvisionedAlertRule](#provisioned-alert-rule)
### <span id="raw-message"></span> RawMessage
[interface{}](#interface)
### <span id="receiver-export"></span> ReceiverExport
**Properties**
| Name | Type | Go type | Required | Default | Description | Example |
| --------------------- | ------------- | -------- | :------: | ------- | ----------- | ------- |
| disableResolveMessage | boolean | `bool` | | | | |
| settings | [JSON](#json) | `JSON` | | | | |
| type | string | `string` | | | | |
| uid | string | `string` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| --------------------- | -------------------------- | ------------ | :------: | ------- | ----------- | ------- |
| disableResolveMessage | boolean | `bool` | | | | |
| settings | [RawMessage](#raw-message) | `RawMessage` | | | | |
| type | string | `string` | | | | |
| uid | string | `string` | | | | |
### <span id="regexp"></span> Regexp
@ -1423,6 +1473,28 @@ Status: Accepted
{{% /responsive-table %}}
### <span id="route-export"></span> RouteExport
> RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in
> provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.
**Properties**
| 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` | | | | |
| receiver | string | `string` | | | | |
| repeat_interval | string | `string` | | | | |
| routes | [][RouteExport](#route-export) | `[]*RouteExport` | | | | |
### <span id="time-interval"></span> TimeInterval
> TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained

View File

@ -80,6 +80,23 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) respo
return response.JSON(http.StatusOK, policies)
}
func (srv *ProvisioningSrv) RouteGetPolicyTreeExport(c *contextmodel.ReqContext) response.Response {
policies, err := srv.policies.GetPolicyTree(c.Req.Context(), c.OrgID)
if err != nil {
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return ErrResp(http.StatusNotFound, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "")
}
e, err := AlertingFileExportFromRoute(c.OrgID, policies)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
}
return exportResponse(c, e)
}
func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree definitions.Route) response.Response {
provenance := determineProvenance(c)
err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgID, tree, alerting_models.Provenance(provenance))

View File

@ -11,6 +11,7 @@ import (
"time"
prometheus "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/mock"
@ -812,6 +813,116 @@ func TestProvisioningApi(t *testing.T) {
require.Equal(t, expectedResponse, string(response.Body()))
})
})
t.Run("notification policies", func(t *testing.T) {
t.Run("are present, GET returns 200", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
response := sut.RouteGetPolicyTreeExport(&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()
rc.Context.Req.Header.Add("Accept", "application/yaml")
response := sut.RouteGetPolicyTreeExport(&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()
rc.Context.Req.Header.Add("Accept", "application/json")
response := sut.RouteGetPolicyTreeExport(&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()
rc.Context.Req.Header.Add("Accept", "application/json, application/yaml")
response := sut.RouteGetPolicyTreeExport(&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()
rc.Context.Req.Form.Set("download", "true")
response := sut.RouteGetPolicyTreeExport(&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()
rc.Context.Req.Form.Set("download", "false")
response := sut.RouteGetPolicyTreeExport(&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()
response := sut.RouteGetPolicyTreeExport(&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)
sut.policies = createFakeNotificationPolicyService()
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/json")
expectedResponse := `{"apiVersion":1,"policies":[{"orgId":1,"Policy":{"receiver":"default-receiver","group_by":["g1","g2"],"routes":[{"receiver":"nested-receiver","group_by":["g3","g4"],"matchers":["a=\"b\""],"object_matchers":[["foo","=","bar"]],"mute_time_intervals":["interval"],"continue":true,"group_wait":"5m","group_interval":"5m","repeat_interval":"5m"}],"group_wait":"30s","group_interval":"5m","repeat_interval":"1h"}}]}`
response := sut.RouteGetPolicyTreeExport(&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)
sut.policies = createFakeNotificationPolicyService()
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\npolicies:\n - orgId: 1\n receiver: default-receiver\n group_by:\n - g1\n - g2\n routes:\n - receiver: nested-receiver\n group_by:\n - g3\n - g4\n matchers:\n - a=\"b\"\n object_matchers:\n - - foo\n - =\n - bar\n mute_time_intervals:\n - interval\n continue: true\n group_wait: 5m\n group_interval: 5m\n repeat_interval: 5m\n group_wait: 30s\n group_interval: 5m\n repeat_interval: 1h\n"
response := sut.RouteGetPolicyTreeExport(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body()))
})
})
})
}
@ -1162,6 +1273,39 @@ func newFakeNotificationPolicyService() *fakeNotificationPolicyService {
}
}
func createFakeNotificationPolicyService() *fakeNotificationPolicyService {
seconds := model.Duration(time.Duration(30) * time.Second)
minutes := model.Duration(time.Duration(5) * time.Minute)
hours := model.Duration(time.Duration(1) * time.Hour)
return &fakeNotificationPolicyService{
tree: definitions.Route{
Receiver: "default-receiver",
GroupByStr: []string{"g1", "g2"},
GroupWait: &seconds,
GroupInterval: &minutes,
RepeatInterval: &hours,
Routes: []*definitions.Route{{
Receiver: "nested-receiver",
GroupByStr: []string{"g3", "g4"},
Matchers: prometheus.Matchers{
{
Name: "a",
Type: labels.MatchEqual,
Value: "b",
},
},
ObjectMatchers: definitions.ObjectMatchers{{Type: 0, Name: "foo", Value: "bar"}},
MuteTimeIntervals: []string{"interval"},
Continue: true,
GroupWait: &minutes,
GroupInterval: &minutes,
RepeatInterval: &minutes,
}},
},
prov: models.ProvenanceAPI,
}
}
func (f *fakeNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) {
if orgID != 1 {
return definitions.Route{}, store.ErrNoAlertmanagerConfiguration

View File

@ -185,6 +185,7 @@ func (api *API) authorize(method, path string) web.Handler {
// Grafana-only Provisioning Read Paths
case http.MethodGet + "/api/v1/provisioning/policies",
http.MethodGet + "/api/v1/provisioning/policies/export",
http.MethodGet + "/api/v1/provisioning/contact-points",
http.MethodGet + "/api/v1/provisioning/contact-points/export",
http.MethodGet + "/api/v1/provisioning/templates",

View File

@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 49)
require.Len(t, paths, 50)
ac := acmock.New()
api := &API{AccessControl: ac}

View File

@ -260,3 +260,41 @@ func ReceiverExportFromEmbeddedContactPoint(contact definitions.EmbeddedContactP
DisableResolveMessage: contact.DisableResolveMessage,
}, nil
}
// AlertingFileExportFromRoute creates a definitions.AlertingFileExport DTO from definitions.Route.
func AlertingFileExportFromRoute(orgID int64, route definitions.Route) (definitions.AlertingFileExport, error) {
f := definitions.AlertingFileExport{
APIVersion: 1,
Policies: []definitions.NotificationPolicyExport{{
OrgID: orgID,
Policy: RouteExportFromRoute(&route),
}},
}
return f, nil
}
// RouteExportFromRoute creates a definitions.RouteExport DTO from definitions.Route.
func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport {
export := definitions.RouteExport{
Receiver: route.Receiver,
GroupByStr: route.GroupByStr,
Match: route.Match,
MatchRE: route.MatchRE,
Matchers: route.Matchers,
ObjectMatchers: route.ObjectMatchers,
MuteTimeIntervals: route.MuteTimeIntervals,
Continue: route.Continue,
GroupWait: route.GroupWait,
GroupInterval: route.GroupInterval,
RepeatInterval: route.RepeatInterval,
}
if len(route.Routes) > 0 {
export.Routes = make([]*definitions.RouteExport, 0, len(route.Routes))
for _, r := range route.Routes {
export.Routes = append(export.Routes, RouteExportFromRoute(r))
}
}
return &export
}

View File

@ -34,6 +34,7 @@ type ProvisioningApi interface {
RouteGetMuteTiming(*contextmodel.ReqContext) response.Response
RouteGetMuteTimings(*contextmodel.ReqContext) response.Response
RouteGetPolicyTree(*contextmodel.ReqContext) response.Response
RouteGetPolicyTreeExport(*contextmodel.ReqContext) response.Response
RouteGetTemplate(*contextmodel.ReqContext) response.Response
RouteGetTemplates(*contextmodel.ReqContext) response.Response
RoutePostAlertRule(*contextmodel.ReqContext) response.Response
@ -113,6 +114,9 @@ func (f *ProvisioningApiHandler) RouteGetMuteTimings(ctx *contextmodel.ReqContex
func (f *ProvisioningApiHandler) RouteGetPolicyTree(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteGetPolicyTree(ctx)
}
func (f *ProvisioningApiHandler) RouteGetPolicyTreeExport(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteGetPolicyTreeExport(ctx)
}
func (f *ProvisioningApiHandler) RouteGetTemplate(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
@ -360,6 +364,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/policies/export"),
api.authorize(http.MethodGet, "/api/v1/provisioning/policies/export"),
metrics.Instrument(
http.MethodGet,
"/api/v1/provisioning/policies/export",
api.Hooks.Wrap(srv.RouteGetPolicyTreeExport),
m,
),
)
group.Get(
toMacaronPath("/api/v1/provisioning/templates/{name}"),
api.authorize(http.MethodGet, "/api/v1/provisioning/templates/{name}"),

View File

@ -20,6 +20,10 @@ func (f *ProvisioningApiHandler) handleRouteGetPolicyTree(ctx *contextmodel.ReqC
return f.svc.RouteGetPolicyTree(ctx)
}
func (f *ProvisioningApiHandler) handleRouteGetPolicyTreeExport(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteGetPolicyTreeExport(ctx)
}
func (f *ProvisioningApiHandler) handleRoutePutPolicyTree(ctx *contextmodel.ReqContext, route apimodels.Route) response.Response {
return f.svc.RoutePutPolicyTree(ctx, route)
}

View File

@ -297,6 +297,12 @@
"$ref": "#/definitions/AlertRuleGroupExport"
},
"type": "array"
},
"policies": {
"items": {
"$ref": "#/definitions/NotificationPolicyExport"
},
"type": "array"
}
},
"title": "AlertingFileExport is the full provisioned file export.",
@ -1097,6 +1103,10 @@
"description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.",
"type": "string"
},
"preferredVisualisationPluginId": {
"description": "PreferredVisualizationPluginId sets the panel plugin id to use to render the data when using Explore. If\nthe plugin cannot be found will fall back to PreferredVisualization.",
"type": "string"
},
"preferredVisualisationType": {
"$ref": "#/definitions/VisType"
},
@ -1897,6 +1907,19 @@
"title": "NoticeSeverity is a type for the Severity property of a Notice.",
"type": "integer"
},
"NotificationPolicyExport": {
"properties": {
"Policy": {
"$ref": "#/definitions/RouteExport"
},
"orgId": {
"format": "int64",
"type": "integer"
}
},
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
"type": "object"
},
"NotificationTemplate": {
"properties": {
"name": {
@ -2882,7 +2905,7 @@
"type": "boolean"
},
"settings": {
"$ref": "#/definitions/Json"
"$ref": "#/definitions/RawMessage"
},
"type": {
"type": "string"
@ -2985,6 +3008,61 @@
},
"type": "object"
},
"RouteExport": {
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
"properties": {
"continue": {
"type": "boolean"
},
"group_by": {
"items": {
"type": "string"
},
"type": "array"
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"additionalProperties": {
"type": "string"
},
"description": "Deprecated. Remove before v1.0 release.",
"type": "object"
},
"match_re": {
"$ref": "#/definitions/MatchRegexps"
},
"matchers": {
"$ref": "#/definitions/Matchers"
},
"mute_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"items": {
"$ref": "#/definitions/RouteExport"
},
"type": "array"
}
},
"type": "object"
},
"Rule": {
"description": "adapted from cortex",
"properties": {
@ -4185,7 +4263,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",
@ -5237,6 +5314,29 @@
]
}
},
"/api/v1/provisioning/policies/export": {
"get": {
"operationId": "RouteGetPolicyTreeExport",
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Export the notification policy tree in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/templates": {
"get": {
"operationId": "RouteGetTemplates",

View File

@ -3,9 +3,10 @@ package definitions
// AlertingFileExport is the full provisioned file export.
// swagger:model
type AlertingFileExport struct {
APIVersion int64 `json:"apiVersion" yaml:"apiVersion"`
Groups []AlertRuleGroupExport `json:"groups,omitempty" yaml:"groups,omitempty"`
ContactPoints []ContactPointExport `json:"contactPoints,omitempty" yaml:"contactPoints,omitempty"`
APIVersion int64 `json:"apiVersion" yaml:"apiVersion"`
Groups []AlertRuleGroupExport `json:"groups,omitempty" yaml:"groups,omitempty"`
ContactPoints []ContactPointExport `json:"contactPoints,omitempty" yaml:"contactPoints,omitempty"`
Policies []NotificationPolicyExport `json:"policies,omitempty" yaml:"policies,omitempty"`
}
// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetAlertRulesExport RouteGetContactpointsExport RouteGetContactpointExport

View File

@ -51,7 +51,7 @@ import (
// Responses:
// 204: description: The contact point was deleted successfully.
// swagger:parameters RoutePutContactpoint RouteDeleteContactpoints RouteGetContactpoint RouteGetContactpointExport
// swagger:parameters RoutePutContactpoint RouteDeleteContactpoints
type ContactPointUIDReference struct {
// UID is the contact point unique identifier
// in:path

View File

@ -1,5 +1,10 @@
package definitions
import (
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/common/model"
)
// swagger:route GET /api/v1/provisioning/policies provisioning stable RouteGetPolicyTree
//
// Get the notification policy tree.
@ -29,9 +34,44 @@ package definitions
// Responses:
// 202: Ack
// swagger:route GET /api/v1/provisioning/policies/export provisioning stable RouteGetPolicyTreeExport
//
// Export the notification policy tree in provisioning file format.
//
// Responses:
// 200: AlertingFileExport
// 404: NotFound
// swagger:parameters RoutePutPolicyTree
type Policytree struct {
// The new notification routing tree to use
// in:body
Body Route
}
// NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.
type NotificationPolicyExport struct {
OrgID int64 `json:"orgId" yaml:"orgId"`
Policy *RouteExport `json:",inline" yaml:",inline"`
}
// RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in
// provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.
type RouteExport struct {
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"`
GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"`
// Deprecated. Remove before v1.0 release.
Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"`
// Deprecated. Remove before v1.0 release.
MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"`
Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"`
ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"`
MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"`
Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` // Added omitempty to yaml for a cleaner export.
Routes []*RouteExport `yaml:"routes,omitempty" json:"routes,omitempty"`
GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"`
GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"`
RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"`
}

View File

@ -297,6 +297,12 @@
"$ref": "#/definitions/AlertRuleGroupExport"
},
"type": "array"
},
"policies": {
"items": {
"$ref": "#/definitions/NotificationPolicyExport"
},
"type": "array"
}
},
"title": "AlertingFileExport is the full provisioned file export.",
@ -1097,6 +1103,10 @@
"description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.",
"type": "string"
},
"preferredVisualisationPluginId": {
"description": "PreferredVisualizationPluginId sets the panel plugin id to use to render the data when using Explore. If\nthe plugin cannot be found will fall back to PreferredVisualization.",
"type": "string"
},
"preferredVisualisationType": {
"$ref": "#/definitions/VisType"
},
@ -1897,6 +1907,19 @@
"title": "NoticeSeverity is a type for the Severity property of a Notice.",
"type": "integer"
},
"NotificationPolicyExport": {
"properties": {
"Policy": {
"$ref": "#/definitions/RouteExport"
},
"orgId": {
"format": "int64",
"type": "integer"
}
},
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
"type": "object"
},
"NotificationTemplate": {
"properties": {
"name": {
@ -2882,7 +2905,7 @@
"type": "boolean"
},
"settings": {
"$ref": "#/definitions/Json"
"$ref": "#/definitions/RawMessage"
},
"type": {
"type": "string"
@ -2985,6 +3008,61 @@
},
"type": "object"
},
"RouteExport": {
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
"properties": {
"continue": {
"type": "boolean"
},
"group_by": {
"items": {
"type": "string"
},
"type": "array"
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"additionalProperties": {
"type": "string"
},
"description": "Deprecated. Remove before v1.0 release.",
"type": "object"
},
"match_re": {
"$ref": "#/definitions/MatchRegexps"
},
"matchers": {
"$ref": "#/definitions/Matchers"
},
"mute_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"items": {
"$ref": "#/definitions/RouteExport"
},
"type": "array"
}
},
"type": "object"
},
"Rule": {
"description": "adapted from cortex",
"properties": {
@ -3940,6 +4018,7 @@
"type": "object"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@ -3963,6 +4042,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup"
},
@ -4067,7 +4147,6 @@
"type": "object"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/definitions/labelSet"
@ -4328,7 +4407,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -7017,6 +7095,29 @@
]
}
},
"/api/v1/provisioning/policies/export": {
"get": {
"operationId": "RouteGetPolicyTreeExport",
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Export the notification policy tree in provisioning file format.",
"tags": [
"provisioning"
]
}
},
"/api/v1/provisioning/templates": {
"get": {
"operationId": "RouteGetTemplates",

View File

@ -2572,6 +2572,30 @@
}
}
},
"/api/v1/provisioning/policies/export": {
"get": {
"tags": [
"provisioning",
"stable"
],
"summary": "Export the notification policy tree in provisioning file format.",
"operationId": "RouteGetPolicyTreeExport",
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
}
},
"/api/v1/provisioning/templates": {
"get": {
"tags": [
@ -3118,6 +3142,12 @@
"items": {
"$ref": "#/definitions/AlertRuleGroupExport"
}
},
"policies": {
"type": "array",
"items": {
"$ref": "#/definitions/NotificationPolicyExport"
}
}
}
},
@ -3921,6 +3951,10 @@
"description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.",
"type": "string"
},
"preferredVisualisationPluginId": {
"description": "PreferredVisualizationPluginId sets the panel plugin id to use to render the data when using Explore. If\nthe plugin cannot be found will fall back to PreferredVisualization.",
"type": "string"
},
"preferredVisualisationType": {
"$ref": "#/definitions/VisType"
},
@ -4720,6 +4754,19 @@
"format": "int64",
"title": "NoticeSeverity is a type for the Severity property of a Notice."
},
"NotificationPolicyExport": {
"type": "object",
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
"properties": {
"Policy": {
"$ref": "#/definitions/RouteExport"
},
"orgId": {
"type": "integer",
"format": "int64"
}
}
},
"NotificationTemplate": {
"type": "object",
"properties": {
@ -5707,7 +5754,7 @@
"type": "boolean"
},
"settings": {
"$ref": "#/definitions/Json"
"$ref": "#/definitions/RawMessage"
},
"type": {
"type": "string"
@ -5808,6 +5855,61 @@
}
}
},
"RouteExport": {
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
"type": "object",
"properties": {
"continue": {
"type": "boolean"
},
"group_by": {
"type": "array",
"items": {
"type": "string"
}
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"description": "Deprecated. Remove before v1.0 release.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"match_re": {
"$ref": "#/definitions/MatchRegexps"
},
"matchers": {
"$ref": "#/definitions/Matchers"
},
"mute_time_intervals": {
"type": "array",
"items": {
"type": "string"
}
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"type": "array",
"items": {
"$ref": "#/definitions/RouteExport"
}
}
}
},
"Rule": {
"description": "adapted from cortex",
"type": "object",
@ -6763,6 +6865,7 @@
}
},
"alertGroup": {
"description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@ -6787,6 +6890,7 @@
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"$ref": "#/definitions/alertGroup"
@ -6892,7 +6996,6 @@
}
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object",
"required": [
"labels",
@ -7158,7 +7261,6 @@
}
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",

View File

@ -13,11 +13,14 @@ import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
import { RouteWithID, Receiver, ObjectMatcher, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
import { ReceiversState } from 'app/types';
import { isOrgAdmin } from '../../../../plugins/admin/permissions';
import { INTEGRATION_ICONS } from '../../types/contact-points';
import { getNotificationsPermissions } from '../../utils/access-control';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { normalizeMatchers } from '../../utils/matchers';
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies';
import { createUrl } from '../../utils/url';
import { HoverCard } from '../HoverCard';
import { Label } from '../Label';
import { MetaText } from '../MetaText';
@ -72,6 +75,7 @@ const Policy: FC<PolicyComponentProps> = ({
const permissions = getNotificationsPermissions(alertManagerSourceName);
const canEditRoutes = contextSrv.hasPermission(permissions.update);
const canDeleteRoutes = contextSrv.hasPermission(permissions.delete);
const canReadProvisioning = contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin());
const contactPoint = currentRoute.receiver;
const continueMatching = currentRoute.continue ?? false;
@ -122,6 +126,9 @@ const Policy: FC<PolicyComponentProps> = ({
? sumBy(matchingAlertGroups, (group) => group.alerts.length)
: undefined;
const isGrafanaAM = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
const showExport = isGrafanaAM && isDefaultPolicy && canReadProvisioning;
// TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated
return (
<Stack direction="column" gap={1.5}>
@ -148,56 +155,73 @@ const Policy: FC<PolicyComponentProps> = ({
{/* TODO maybe we should move errors to the gutter instead? */}
{errors.length > 0 && <Errors errors={errors} />}
{provisioned && <ProvisioningBadge />}
{readOnly ? null : (
{readOnly && !showExport ? null : (
<Stack direction="row" gap={0.5}>
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
<Button
variant="secondary"
icon="plus"
size="sm"
onClick={() => onAddPolicy(currentRoute)}
disabled={provisioned}
type="button"
>
New nested policy
</Button>
</ConditionalWrap>
{!readOnly && (
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
<Button
variant="secondary"
icon="plus"
size="sm"
onClick={() => onAddPolicy(currentRoute)}
disabled={provisioned}
type="button"
>
New nested policy
</Button>
</ConditionalWrap>
)}
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
<Dropdown
overlay={
<Menu>
<Dropdown
overlay={
<Menu>
{!readOnly && (
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
<Menu.Item
icon="edit"
disabled={!isEditable || provisioned}
label="Edit"
onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)}
/>
</ConditionalWrap>
)}
{showExport && (
<Menu.Item
icon="edit"
disabled={!isEditable}
label="Edit"
onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)}
icon="download-alt"
label="Export"
url={createUrl('/api/v1/provisioning/policies/export', {
download: 'true',
format: 'yaml',
})}
target="_blank"
/>
{isDeletable && (
<>
<Menu.Divider />
)}
{!readOnly && isDeletable && (
<>
<Menu.Divider />
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
<Menu.Item
destructive
icon="trash-alt"
disabled={!isDeletable || provisioned}
label="Delete"
onClick={() => onDeletePolicy(currentRoute)}
/>
</>
)}
</Menu>
}
>
<Button
icon="ellipsis-h"
variant="secondary"
size="sm"
type="button"
aria-label="more-actions"
data-testid="more-actions"
disabled={provisioned}
/>
</Dropdown>
</ConditionalWrap>
</ConditionalWrap>
</>
)}
</Menu>
}
>
<Button
icon="ellipsis-h"
variant="secondary"
size="sm"
type="button"
aria-label="more-actions"
data-testid="more-actions"
/>
</Dropdown>
</Stack>
)}
</Stack>