Alerting/allow empty receiver (#33962)

* simplifies yaml unmarshaling: PostableApiReceiver

* allow empty receiver type

* allows name only receivers (blackhole)

* better receiver type parsing

* linting
This commit is contained in:
Owen Diehl 2021-05-12 07:58:16 -04:00 committed by GitHub
parent 7a55a6385c
commit 3b06f52bab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 229 additions and 43 deletions

View File

@ -117,22 +117,16 @@ func (am *ForkedAMSvc) RoutePostAlertingConfig(ctx *models.ReqContext, body apim
return response.Error(400, err.Error(), nil) return response.Error(400, err.Error(), nil)
} }
backendType, err := backendType(ctx, am.DatasourceCache) b, err := backendType(ctx, am.DatasourceCache)
if err != nil { if err != nil {
return response.Error(400, err.Error(), nil) return response.Error(400, err.Error(), nil)
} }
payloadType := body.AlertmanagerConfig.Type() if err := body.AlertmanagerConfig.ReceiverType().MatchesBackend(b); err != nil {
if backendType != payloadType {
return response.Error( return response.Error(
400, 400,
fmt.Sprintf( "bad match",
"unexpected backend type (%v) vs payload type (%v)", err,
backendType,
payloadType,
),
nil,
) )
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
@ -381,6 +382,8 @@ func (c *GettableApiAlertingConfig) validate() error {
hasGrafReceivers = true hasGrafReceivers = true
case AlertmanagerReceiverType: case AlertmanagerReceiverType:
hasAMReceivers = true hasAMReceivers = true
default:
continue
} }
} }
@ -398,19 +401,6 @@ func (c *GettableApiAlertingConfig) validate() error {
return nil return nil
} }
// Type requires validate has been called and just checks the first receiver type
func (c *GettableApiAlertingConfig) Type() (backend Backend) {
for _, r := range c.Receivers {
switch r.Type() {
case GrafanaReceiverType:
return GrafanaBackend
case AlertmanagerReceiverType:
return AlertmanagerBackend
}
}
return
}
// Config is the top-level configuration for Alertmanager's config files. // Config is the top-level configuration for Alertmanager's config files.
type Config struct { type Config struct {
Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"`
@ -448,6 +438,8 @@ func (c *PostableApiAlertingConfig) validate() error {
hasGrafReceivers = true hasGrafReceivers = true
case AlertmanagerReceiverType: case AlertmanagerReceiverType:
hasAMReceivers = true hasAMReceivers = true
default:
continue
} }
} }
@ -466,22 +458,26 @@ func (c *PostableApiAlertingConfig) validate() error {
} }
// Type requires validate has been called and just checks the first receiver type // Type requires validate has been called and just checks the first receiver type
func (c *PostableApiAlertingConfig) Type() (backend Backend) { func (c *PostableApiAlertingConfig) ReceiverType() ReceiverType {
for _, r := range c.Receivers { for _, r := range c.Receivers {
switch r.Type() { switch r.Type() {
case GrafanaReceiverType: case GrafanaReceiverType:
return GrafanaBackend return GrafanaReceiverType
case AlertmanagerReceiverType: case AlertmanagerReceiverType:
return AlertmanagerBackend return AlertmanagerReceiverType
default:
continue
} }
} }
return return EmptyReceiverType
} }
// AllReceivers will recursively walk a routing tree and return a list of all the // AllReceivers will recursively walk a routing tree and return a list of all the
// referenced receiver names. // referenced receiver names.
func AllReceivers(route *config.Route) (res []string) { func AllReceivers(route *config.Route) (res []string) {
res = append(res, route.Receiver) if route.Receiver != "" {
res = append(res, route.Receiver)
}
for _, subRoute := range route.Routes { for _, subRoute := range route.Routes {
res = append(res, AllReceivers(subRoute)...) res = append(res, AllReceivers(subRoute)...)
} }
@ -494,10 +490,52 @@ type PostableGrafanaReceiver models.CreateAlertNotificationCommand
type ReceiverType int type ReceiverType int
const ( const (
GrafanaReceiverType ReceiverType = iota GrafanaReceiverType ReceiverType = 1 << iota
AlertmanagerReceiverType AlertmanagerReceiverType
EmptyReceiverType = GrafanaReceiverType | AlertmanagerReceiverType
) )
func (r ReceiverType) String() string {
switch r {
case GrafanaReceiverType:
return "grafana"
case AlertmanagerReceiverType:
return "alertmanager"
case EmptyReceiverType:
return "empty"
default:
return "unknown"
}
}
// Can determines whether a receiver type can implement another receiver type.
// This is useful as receivers with just names but no contact points
// are valid in all backends.
func (r ReceiverType) Can(other ReceiverType) bool { return r&other != 0 }
// MatchesBackend determines if a config payload can be sent to a particular backend type
func (r ReceiverType) MatchesBackend(backend Backend) error {
msg := func(backend Backend, receiver ReceiverType) error {
return fmt.Errorf(
"unexpected backend type (%s) for receiver type (%s)",
backend.String(),
receiver.String(),
)
}
var ok bool
switch backend {
case GrafanaBackend:
ok = r.Can(GrafanaReceiverType)
case AlertmanagerBackend:
ok = r.Can(AlertmanagerReceiverType)
default:
}
if !ok {
return msg(backend, r)
}
return nil
}
type GettableApiReceiver struct { type GettableApiReceiver struct {
config.Receiver `yaml:",inline"` config.Receiver `yaml:",inline"`
GettableGrafanaReceivers `yaml:",inline"` GettableGrafanaReceivers `yaml:",inline"`
@ -554,25 +592,14 @@ type PostableApiReceiver struct {
} }
func (r *PostableApiReceiver) UnmarshalYAML(unmarshal func(interface{}) error) error { func (r *PostableApiReceiver) UnmarshalYAML(unmarshal func(interface{}) error) error {
var grafanaReceivers PostableGrafanaReceivers if err := unmarshal(&r.PostableGrafanaReceivers); err != nil {
if err := unmarshal(&grafanaReceivers); err != nil {
return err return err
} }
r.PostableGrafanaReceivers = grafanaReceivers
var cfg config.Receiver if err := unmarshal(&r.Receiver); err != nil {
if err := unmarshal(&cfg); err != nil {
return err return err
} }
r.Name = cfg.Name
r.EmailConfigs = cfg.EmailConfigs
r.PagerdutyConfigs = cfg.PagerdutyConfigs
r.SlackConfigs = cfg.SlackConfigs
r.WebhookConfigs = cfg.WebhookConfigs
r.OpsGenieConfigs = cfg.OpsGenieConfigs
r.WechatConfigs = cfg.WechatConfigs
r.PushoverConfigs = cfg.PushoverConfigs
r.VictorOpsConfigs = cfg.VictorOpsConfigs
return nil return nil
} }
@ -617,6 +644,13 @@ func (r *PostableApiReceiver) Type() ReceiverType {
if len(r.PostableGrafanaReceivers.GrafanaManagedReceivers) > 0 { if len(r.PostableGrafanaReceivers.GrafanaManagedReceivers) > 0 {
return GrafanaReceiverType return GrafanaReceiverType
} }
cpy := r.Receiver
cpy.Name = ""
if reflect.ValueOf(cpy).IsZero() {
return EmptyReceiverType
}
return AlertmanagerReceiverType return AlertmanagerReceiverType
} }

View File

@ -69,6 +69,50 @@ func Test_ApiReceiver_Marshaling(t *testing.T) {
} }
} }
func Test_APIReceiverType(t *testing.T) {
for _, tc := range []struct {
desc string
input PostableApiReceiver
expected ReceiverType
}{
{
desc: "empty",
input: PostableApiReceiver{
Receiver: config.Receiver{
Name: "foo",
},
},
expected: EmptyReceiverType,
},
{
desc: "am",
input: PostableApiReceiver{
Receiver: config.Receiver{
Name: "foo",
EmailConfigs: []*config.EmailConfig{{}},
},
},
expected: AlertmanagerReceiverType,
},
{
desc: "graf",
input: PostableApiReceiver{
Receiver: config.Receiver{
Name: "foo",
},
PostableGrafanaReceivers: PostableGrafanaReceivers{
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}},
},
},
expected: GrafanaReceiverType,
},
} {
t.Run(tc.desc, func(t *testing.T) {
require.Equal(t, tc.expected, tc.input.Type())
})
}
}
func Test_AllReceivers(t *testing.T) { func Test_AllReceivers(t *testing.T) {
input := &config.Route{ input := &config.Route{
Receiver: "foo", Receiver: "foo",
@ -88,6 +132,10 @@ func Test_AllReceivers(t *testing.T) {
} }
require.Equal(t, []string{"foo", "bar", "bazz", "buzz"}, AllReceivers(input)) require.Equal(t, []string{"foo", "bar", "bazz", "buzz"}, AllReceivers(input))
// test empty
var empty []string
require.Equal(t, empty, AllReceivers(&config.Route{}))
} }
func Test_ApiAlertingConfig_Marshaling(t *testing.T) { func Test_ApiAlertingConfig_Marshaling(t *testing.T) {
@ -405,3 +453,113 @@ func Test_GettableUserConfigRoundtrip(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, string(yamlEncoded), string(out)) require.Equal(t, string(yamlEncoded), string(out))
} }
func Test_ReceiverCompatibility(t *testing.T) {
for _, tc := range []struct {
desc string
a, b ReceiverType
expected bool
}{
{
desc: "grafana=grafana",
a: GrafanaReceiverType,
b: GrafanaReceiverType,
expected: true,
},
{
desc: "am=am",
a: AlertmanagerReceiverType,
b: AlertmanagerReceiverType,
expected: true,
},
{
desc: "empty=grafana",
a: EmptyReceiverType,
b: AlertmanagerReceiverType,
expected: true,
},
{
desc: "empty=am",
a: EmptyReceiverType,
b: AlertmanagerReceiverType,
expected: true,
},
{
desc: "empty=empty",
a: EmptyReceiverType,
b: EmptyReceiverType,
expected: true,
},
{
desc: "graf!=am",
a: GrafanaReceiverType,
b: AlertmanagerReceiverType,
expected: false,
},
{
desc: "am!=graf",
a: AlertmanagerReceiverType,
b: GrafanaReceiverType,
expected: false,
},
} {
t.Run(tc.desc, func(t *testing.T) {
require.Equal(t, tc.expected, tc.a.Can(tc.b))
})
}
}
func Test_ReceiverMatchesBackend(t *testing.T) {
for _, tc := range []struct {
desc string
rec ReceiverType
b Backend
err bool
}{
{
desc: "graf=graf",
rec: GrafanaReceiverType,
b: GrafanaBackend,
err: false,
},
{
desc: "empty=graf",
rec: EmptyReceiverType,
b: GrafanaBackend,
err: false,
},
{
desc: "am=am",
rec: AlertmanagerReceiverType,
b: AlertmanagerBackend,
err: false,
},
{
desc: "empty=am",
rec: EmptyReceiverType,
b: AlertmanagerBackend,
err: false,
},
{
desc: "graf!=am",
rec: GrafanaReceiverType,
b: AlertmanagerBackend,
err: true,
},
{
desc: "am!=ruler",
rec: GrafanaReceiverType,
b: LoTexRulerBackend,
err: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
err := tc.rec.MatchesBackend(tc.b)
if tc.err {
require.NotNil(t, err)
} else {
require.Nil(t, err)
}
})
}
}