Merge pull request #14 from grafana/lib/validation

Validation & Routing
This commit is contained in:
Owen Diehl
2021-02-25 09:03:49 -05:00
committed by GitHub
7 changed files with 499 additions and 9 deletions
+2 -1
View File
@@ -5,7 +5,8 @@ GO_PKG_FILES = $(shell find $(PKG_DIR) -name *.go -print)
spec.json: $(GO_PKG_FILES)
swagger generate spec -m -w $(PKG_DIR) -o $@
swagger generate spec -m -w $(PKG_DIR) | jq 'del(.definitions.ApiAlertingConfig.properties.route)' > $@
.PHONY: openapi
openapi: spec.json
+2 -1
View File
@@ -2,7 +2,7 @@
[view api](https://grafana.github.io/alerting-api/)
This repo aims to define the unified alerting API as code. It generates OpenAPI definitions from go structs, initially pulled from
This repo aims to define the unified alerting API as code. It generates OpenAPI definitions from go structs
## Running
@@ -11,3 +11,4 @@ This repo aims to define the unified alerting API as code. It generates OpenAPI
## Requires
- [go-swagger](https://github.com/go-swagger/go-swagger)
- [jq](https://stedolan.github.io/jq/)
+1
View File
@@ -11,6 +11,7 @@ require (
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/common v0.15.0
github.com/prometheus/prometheus v1.8.2-0.20201014093524-73e2ce1bd643
github.com/stretchr/testify v1.7.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/oauth2 v0.0.0-20210210192628-66670185b0cd // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
+119
View File
@@ -1,6 +1,9 @@
package api
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/models"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/config"
@@ -171,13 +174,129 @@ type AlertingConfigResponse struct {
type ApiAlertingConfig struct {
config.Config
// TODO: PR a followup to https://github.com/go-swagger/go-swagger/pull/1527 in order to allow
// explicitly ignoring embedded fields. In the meantime, these are hackily removed in our make targets via `jq`
AlertManagerRoute *config.Route `yaml:"alertmanager_route,omitempty" json:"alertmanager_route,omitempty"`
GrafanaManagedRoute *config.Route `yaml:"grafana_managed_route,omitempty" json:"grafana_managed_route,omitempty"`
// Override with our superset receiver type
Receivers []*ApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
}
func (c *ApiAlertingConfig) UnmarshalJSON(b []byte) error {
type plain ApiAlertingConfig
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
return err
}
return c.validate()
}
// validate ensures that the two routing trees use the correct receiver types.
func (c *ApiAlertingConfig) validate() error {
receivers := make(map[string]ReceiverType, len(c.Receivers))
for _, r := range c.Receivers {
receivers[r.Name] = r.Type()
}
for _, receiver := range AllReceivers(c.GrafanaManagedRoute) {
t, ok := receivers[receiver]
if !ok {
return fmt.Errorf("unexpected receiver (%s) is undefined", receiver)
}
if t != GrafanaReceiverType {
return fmt.Errorf("unexpected receiver (%s): cannot use Alertmanager receiver types in Grafana managed routes", receiver)
}
}
for _, receiver := range AllReceivers(c.AlertManagerRoute) {
t, ok := receivers[receiver]
if !ok {
return fmt.Errorf("unexpected receiver (%s) is undefined", receiver)
}
if t != AlertmanagerReceiverType {
return fmt.Errorf("unexpected receiver (%s): cannot use Grafana receiver types in non-Grafana managed routes", receiver)
}
}
return nil
}
// AllReceivers will recursively walk a routing tree and return a list of all the
// referenced receiver names.
func AllReceivers(route *config.Route) (res []string) {
res = append(res, route.Receiver)
for _, subRoute := range route.Routes {
res = append(res, AllReceivers(subRoute)...)
}
return res
}
type GrafanaReceiver models.CreateAlertNotificationCommand
type ReceiverType int
const (
GrafanaReceiverType ReceiverType = iota
AlertmanagerReceiverType
)
type ApiReceiver struct {
config.Receiver
GrafanaReceivers
}
func (r *ApiReceiver) UnmarshalJSON(b []byte) error {
type plain ApiReceiver
if err := json.Unmarshal(b, (*plain)(r)); err != nil {
return err
}
hasGrafanaReceivers := len(r.GrafanaReceivers.GrafanaManagedReceivers) > 0
if hasGrafanaReceivers {
if len(r.EmailConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager EmailConfigs & Grafana receivers together")
}
if len(r.PagerdutyConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager PagerdutyConfigs & Grafana receivers together")
}
if len(r.SlackConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager SlackConfigs & Grafana receivers together")
}
if len(r.WebhookConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager WebhookConfigs & Grafana receivers together")
}
if len(r.OpsGenieConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager OpsGenieConfigs & Grafana receivers together")
}
if len(r.WechatConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager WechatConfigs & Grafana receivers together")
}
if len(r.PushoverConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager PushoverConfigs & Grafana receivers together")
}
if len(r.VictorOpsConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager VictorOpsConfigs & Grafana receivers together")
}
}
return nil
}
func (r *ApiReceiver) Type() ReceiverType {
if len(r.GrafanaReceivers.GrafanaManagedReceivers) > 0 {
return GrafanaReceiverType
}
return AlertmanagerReceiverType
}
type GrafanaReceivers struct {
GrafanaManagedReceivers []*GrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"`
}
+303
View File
@@ -0,0 +1,303 @@
package api
import (
"encoding/json"
"testing"
"github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/require"
)
func Test_ApiReceiver_Marshaling(t *testing.T) {
for _, tc := range []struct {
desc string
input ApiReceiver
err bool
}{
{
desc: "success AM",
input: ApiReceiver{
Receiver: config.Receiver{
Name: "foo",
EmailConfigs: []*config.EmailConfig{{}},
},
},
},
{
desc: "success GM",
input: ApiReceiver{
Receiver: config.Receiver{
Name: "foo",
},
GrafanaReceivers: GrafanaReceivers{
GrafanaManagedReceivers: []*GrafanaReceiver{{}},
},
},
},
{
desc: "failure mixed",
input: ApiReceiver{
Receiver: config.Receiver{
Name: "foo",
EmailConfigs: []*config.EmailConfig{{}},
},
GrafanaReceivers: GrafanaReceivers{
GrafanaManagedReceivers: []*GrafanaReceiver{{}},
},
},
err: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
encoded, err := json.Marshal(tc.input)
require.Nil(t, err)
var out ApiReceiver
err = json.Unmarshal(encoded, &out)
if tc.err {
require.Error(t, err)
} else {
require.Equal(t, tc.input, out)
}
})
}
}
func Test_AllReceivers(t *testing.T) {
input := &config.Route{
Receiver: "foo",
Routes: []*config.Route{
{
Receiver: "bar",
Routes: []*config.Route{
{
Receiver: "bazz",
},
},
},
{
Receiver: "buzz",
},
},
}
require.Equal(t, []string{"foo", "bar", "bazz", "buzz"}, AllReceivers(input))
}
func Test_ApiAlertingConfig_Marshaling(t *testing.T) {
for _, tc := range []struct {
desc string
input ApiAlertingConfig
err bool
}{
{
desc: "success",
input: ApiAlertingConfig{
Config: config.Config{},
AlertManagerRoute: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "am",
},
},
},
GrafanaManagedRoute: &config.Route{
Receiver: "graf",
Routes: []*config.Route{
{
Receiver: "graf",
},
},
},
Receivers: []*ApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{}},
},
},
{
Receiver: config.Receiver{
Name: "graf",
},
GrafanaReceivers: GrafanaReceivers{
GrafanaManagedReceivers: []*GrafanaReceiver{{}},
},
},
},
},
},
{
desc: "failure undefined am receiver",
input: ApiAlertingConfig{
Config: config.Config{},
AlertManagerRoute: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "unmentioned",
},
},
},
GrafanaManagedRoute: &config.Route{
Receiver: "graf",
Routes: []*config.Route{
{
Receiver: "graf",
},
},
},
Receivers: []*ApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{}},
},
},
{
Receiver: config.Receiver{
Name: "graf",
},
GrafanaReceivers: GrafanaReceivers{
GrafanaManagedReceivers: []*GrafanaReceiver{{}},
},
},
},
},
err: true,
},
{
desc: "failure undefined graf receiver",
input: ApiAlertingConfig{
Config: config.Config{},
AlertManagerRoute: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "am",
},
},
},
GrafanaManagedRoute: &config.Route{
Receiver: "graf",
Routes: []*config.Route{
{
Receiver: "unmentioned",
},
},
},
Receivers: []*ApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{}},
},
},
{
Receiver: config.Receiver{
Name: "graf",
},
GrafanaReceivers: GrafanaReceivers{
GrafanaManagedReceivers: []*GrafanaReceiver{{}},
},
},
},
},
err: true,
},
{
desc: "failure mixed AM in Grafana",
input: ApiAlertingConfig{
Config: config.Config{},
AlertManagerRoute: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "am",
},
},
},
GrafanaManagedRoute: &config.Route{
Receiver: "graf",
Routes: []*config.Route{
{
Receiver: "am",
},
},
},
Receivers: []*ApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{}},
},
},
{
Receiver: config.Receiver{
Name: "graf",
},
GrafanaReceivers: GrafanaReceivers{
GrafanaManagedReceivers: []*GrafanaReceiver{{}},
},
},
},
},
err: true,
},
{
desc: "failure mixed Grafana in AM",
input: ApiAlertingConfig{
Config: config.Config{},
AlertManagerRoute: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "graf",
},
},
},
GrafanaManagedRoute: &config.Route{
Receiver: "graf",
Routes: []*config.Route{
{
Receiver: "graf",
},
},
},
Receivers: []*ApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{}},
},
},
{
Receiver: config.Receiver{
Name: "graf",
},
GrafanaReceivers: GrafanaReceivers{
GrafanaManagedReceivers: []*GrafanaReceiver{{}},
},
},
},
},
err: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
encoded, err := json.Marshal(tc.input)
require.Nil(t, err)
var out ApiAlertingConfig
err = json.Unmarshal(encoded, &out)
if tc.err {
require.Error(t, err)
} else {
require.Equal(t, tc.input, out)
}
})
}
}
+48
View File
@@ -0,0 +1,48 @@
package routing
import (
"github.com/grafana/alerting-api/pkg/api"
"github.com/prometheus/alertmanager/config"
)
// GrafanaAlertingConfig contains only the Grafana managed alerting configurations.
type GrafanaAlertingConfig struct {
Route *config.Route `yaml:"route,omitempty" json:"route,omitempty"`
Templates []string `yaml:"templates" json:"templates"`
Receivers []*GrafanaReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
}
type GrafanaReceiver struct {
// A unique identifier for this receiver.
Name string `yaml:"name" json:"name"`
api.GrafanaReceivers
}
func SplitAlertingConfig(apiConf api.ApiAlertingConfig) (amConfig config.Config, gConfig GrafanaAlertingConfig, err error) {
var gReceivers []*GrafanaReceiver
var amReceivers []*config.Receiver
for _, r := range apiConf.Receivers {
t := r.Type()
if t == api.GrafanaReceiverType {
gReceivers = append(gReceivers, &GrafanaReceiver{
Name: r.Name,
GrafanaReceivers: r.GrafanaReceivers,
})
} else {
amReceivers = append(amReceivers, &r.Receiver)
}
}
// Create Grafana specific config
gConfig.Templates = apiConf.Templates
gConfig.Route = apiConf.GrafanaManagedRoute
gConfig.Receivers = gReceivers
// Create AM specific config
amConfig = apiConf.Config
amConfig.Route = apiConf.AlertManagerRoute
amConfig.Receivers = amReceivers
return amConfig, gConfig, nil
}
+24 -7
View File
@@ -949,9 +949,15 @@
"ApiAlertingConfig": {
"type": "object",
"properties": {
"alertmanager_route": {
"$ref": "#/definitions/Route"
},
"global": {
"$ref": "#/definitions/GlobalConfig"
},
"grafana_managed_route": {
"$ref": "#/definitions/Route"
},
"inhibit_rules": {
"type": "array",
"items": {
@@ -967,9 +973,6 @@
},
"x-go-name": "Receivers"
},
"route": {
"$ref": "#/definitions/Route"
},
"templates": {
"type": "array",
"items": {
@@ -1533,6 +1536,19 @@
"GrafanaReceiver": {
"$ref": "#/definitions/CreateAlertNotificationCommand"
},
"GrafanaReceivers": {
"type": "object",
"properties": {
"grafana_managed_receiver_configs": {
"type": "array",
"items": {
"$ref": "#/definitions/GrafanaReceiver"
},
"x-go-name": "GrafanaManagedReceivers"
}
},
"x-go-package": "github.com/grafana/alerting-api/pkg/api"
},
"HTTPClientConfig": {
"type": "object",
"title": "HTTPClientConfig configures an HTTP client.",
@@ -1648,7 +1664,7 @@
"properties": {
"Expr": {
"type": "string",
"example": "(node_filesystem_avail_bytes{fstype!=\"\",job=\"integrations/node_exporter\"} node_filesystem_size_bytes{fstype!=\"\",job=\"integrations/node_exporter\"} * 100 \u003c 5 and node_filesystem_readonly{fstype!=\"\",job=\"integrations/node_exporter\"} == 0)"
"example": "(node_filesystem_avail_bytes{fstype!=\"\",job=\"integrations/node_exporter\"} node_filesystem_size_bytes{fstype!=\"\",job=\"integrations/node_exporter\"} * 100 < 5 and node_filesystem_readonly{fstype!=\"\",job=\"integrations/node_exporter\"} == 0)"
},
"datasourceUid": {
"description": "DatasourceUID is required if the query will be sent to grafana to be executed",
@@ -2615,8 +2631,9 @@
"x-go-package": "github.com/grafana/alerting-api/pkg/api"
},
"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": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -2649,7 +2666,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"x-go-package": "github.com/prometheus/common/config"
"x-go-package": "net/url"
},
"UpdateDashboardAclCommand": {
"type": "object",
@@ -3473,4 +3490,4 @@
"type": "basic"
}
}
}
}