From 87cabcda8df719152ff4d33104b6ebb130149e5f Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Tue, 23 Feb 2021 16:48:27 -0500 Subject: [PATCH 1/6] [wip] starts validation --- pkg/api/alertmanager.go | 4 ++++ pkg/api/cortex-ruler.go | 24 ++++++++++----------- pkg/validation/alertconfig.go | 34 ++++++++++++++++++++++++++++++ pkg/validation/alertconfig_test.go | 29 +++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 pkg/validation/alertconfig.go create mode 100644 pkg/validation/alertconfig_test.go diff --git a/pkg/api/alertmanager.go b/pkg/api/alertmanager.go index 2e7f8f39e20..c54c7f5f8f2 100644 --- a/pkg/api/alertmanager.go +++ b/pkg/api/alertmanager.go @@ -179,5 +179,9 @@ type GrafanaReceiver models.CreateAlertNotificationCommand type ApiReceiver struct { config.Receiver + GrafanaReceivers +} + +type GrafanaReceivers struct { GrafanaManagedReceivers []*GrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"` } diff --git a/pkg/api/cortex-ruler.go b/pkg/api/cortex-ruler.go index 12b1bb76654..2495cbdf0ed 100644 --- a/pkg/api/cortex-ruler.go +++ b/pkg/api/cortex-ruler.go @@ -1,9 +1,9 @@ package api import ( + "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/prometheus/common/model" - "github.com/prometheus/prometheus/pkg/rulefmt" ) // swagger:route Get /api/v1/rules ruler RouteGetRulesConfig @@ -99,9 +99,9 @@ type RuleGroupConfig struct { } type ApiRuleNode struct { - Record string `yaml:"record,omitempty" json:"record,omitempty"` - Alert string `yaml:"alert,omitempty" json:"alert,omitempty"` - Expr string `yaml:"expr" json:"expr"` + Record string `yaml:"record,omitempty" json:"record,omitempty"` + Alert string `yaml:"alert,omitempty" json:"alert,omitempty"` + Expr string `yaml:"expr" json:"expr"` For model.Duration `yaml:"for,omitempty" json:"for,omitempty"` Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` @@ -114,17 +114,17 @@ type ExtendedRuleNode struct { } // UpsertAlertDefinitionCommand is copy of the unexported struct: -https://github.com/grafana/grafana/blob/debb82e12417e82a0e2bd09e1a450065f884c1bc/pkg/services/ngalert/models.go#L85 +// https://github.com/grafana/grafana/blob/debb82e12417e82a0e2bd09e1a450065f884c1bc/pkg/services/ngalert/models.go#L85 type UpsertAlertDefinitionCommand struct { - Title string `json:"title" yaml:"title"` + Title string `json:"title" yaml:"title"` // OrgID is an obsolete field (it will derive from the x-grafana-org-id header) - OrgID int64 `json:"-" yaml:"-"` + OrgID int64 `json:"-" yaml:"-"` // Condition is the refID of the query or expression to be evaluated - Condition string `json:"condition" yaml:"condition"` + Condition string `json:"condition" yaml:"condition"` // Data is an array of the queries and expressions - Data []eval.AlertQuery `json:"data" yaml:"data"` + Data []eval.AlertQuery `json:"data" yaml:"data"` // IntervalSeconds is an obsolete field (it will derive from the ruleGroup interval) - IntervalSeconds *int64 `json:"-" yaml:"-"` + IntervalSeconds *int64 `json:"-" yaml:"-"` // UID is set only for existing definitions UID string `json:"uid" yaml:"uid"` @@ -158,7 +158,7 @@ type ExtendedUpsertAlertDefinitionCommand struct { ExecutionErrorState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"` Settings map[string]interface{} `json:"settings" yaml:"settings"` // internal state - FolderUID string `json:"-" yaml:"-"` + FolderUID string `json:"-" yaml:"-"` DatasourceUIDs []string `json:"-" yaml:"-"` - RuleGroupUID string `json:"-" yaml:"-"` + RuleGroupUID string `json:"-" yaml:"-"` } diff --git a/pkg/validation/alertconfig.go b/pkg/validation/alertconfig.go new file mode 100644 index 00000000000..35f1fd5d3d5 --- /dev/null +++ b/pkg/validation/alertconfig.go @@ -0,0 +1,34 @@ +package validation + +import ( + "github.com/grafana/alerting-api/pkg/api" + "github.com/prometheus/alertmanager/config" +) + +// GrafanaAlertingConfig contains only the Grafana managed alerting configurations. +type GrafanaAlertingConfig struct { + api.GrafanaReceivers + Route *config.Route `yaml:"route,omitempty" json:"route,omitempty"` + Templates []string `yaml:"templates" json:"templates"` +} + +// func SplitAlertingConfig(apiConf api.ApiAlertingConfig) (amConfig config.Config, gConfig GrafanaAlertingConfig, err error) { + +// } + +// func MergeAlertingConfigs(amConfig config.Config, gConfig GrafanaAlertingConfig) (conf api.ApiAlertingConfig, err error) { +// } + +// Routing trees must either contain only Grafana Managed receiver types or AlertManager receiver types +// func SplitRoutes(routes []*config.Route, receivers map[string]*api.ApiReceiver) (gRoutes, amRoutes []*config.Route, err error) { +// for _, route := range routes { +// } +// } + +func allReceivers(route *config.Route) (res []string) { + res = append(res, route.Receiver) + for _, subRoute := range route.Routes { + res = append(res, allReceivers(subRoute)...) + } + return res +} diff --git a/pkg/validation/alertconfig_test.go b/pkg/validation/alertconfig_test.go new file mode 100644 index 00000000000..48f4f624314 --- /dev/null +++ b/pkg/validation/alertconfig_test.go @@ -0,0 +1,29 @@ +package validation + +import ( + "testing" + + "github.com/prometheus/alertmanager/config" + "github.com/stretchr/testify/require" +) + +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)) +} From 3b8ceda26a47d898f84773d608a7765ea6c37d66 Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Wed, 24 Feb 2021 10:35:03 -0500 Subject: [PATCH 2/6] receiver unmarshaling tests --- go.mod | 1 + pkg/api/alertmanager.go | 50 +++++++++++++++++++++++++++ pkg/api/alertmanager_test.go | 65 ++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 pkg/api/alertmanager_test.go diff --git a/go.mod b/go.mod index d8502021986..d4de040570e 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/prometheus/alertmanager v0.21.1-0.20210211203738-a7ca7b1d2951 github.com/prometheus/client_golang v1.9.0 github.com/prometheus/common v0.15.0 + 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 diff --git a/pkg/api/alertmanager.go b/pkg/api/alertmanager.go index c54c7f5f8f2..581c59614b7 100644 --- a/pkg/api/alertmanager.go +++ b/pkg/api/alertmanager.go @@ -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" @@ -177,11 +180,58 @@ type ApiAlertingConfig struct { type GrafanaReceiver models.CreateAlertNotificationCommand +type ReceiverType int + +const ( + GrafanaManagedReceiver ReceiverType = iota + AMReceiver +) + 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 + +} + type GrafanaReceivers struct { GrafanaManagedReceivers []*GrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"` } diff --git a/pkg/api/alertmanager_test.go b/pkg/api/alertmanager_test.go new file mode 100644 index 00000000000..1a47516d581 --- /dev/null +++ b/pkg/api/alertmanager_test.go @@ -0,0 +1,65 @@ +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) + } + }) + } +} From ded0d4a9694a2f01cccdf077c74cc6923fc3cf19 Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Wed, 24 Feb 2021 11:15:39 -0500 Subject: [PATCH 3/6] ability to split routes between AM & Grafana --- pkg/api/alertmanager.go | 12 ++- pkg/validation/alertconfig.go | 38 ++++++++- pkg/validation/alertconfig_test.go | 127 +++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 7 deletions(-) diff --git a/pkg/api/alertmanager.go b/pkg/api/alertmanager.go index 581c59614b7..5177aeea43c 100644 --- a/pkg/api/alertmanager.go +++ b/pkg/api/alertmanager.go @@ -183,8 +183,8 @@ type GrafanaReceiver models.CreateAlertNotificationCommand type ReceiverType int const ( - GrafanaManagedReceiver ReceiverType = iota - AMReceiver + GrafanaReceiverType ReceiverType = iota + AlertmanagerReceiverType ) type ApiReceiver struct { @@ -227,11 +227,17 @@ func (r *ApiReceiver) UnmarshalJSON(b []byte) error { } } - 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"` } diff --git a/pkg/validation/alertconfig.go b/pkg/validation/alertconfig.go index 35f1fd5d3d5..1b73a722f40 100644 --- a/pkg/validation/alertconfig.go +++ b/pkg/validation/alertconfig.go @@ -1,6 +1,8 @@ package validation import ( + "fmt" + "github.com/grafana/alerting-api/pkg/api" "github.com/prometheus/alertmanager/config" ) @@ -20,10 +22,38 @@ type GrafanaAlertingConfig struct { // } // Routing trees must either contain only Grafana Managed receiver types or AlertManager receiver types -// func SplitRoutes(routes []*config.Route, receivers map[string]*api.ApiReceiver) (gRoutes, amRoutes []*config.Route, err error) { -// for _, route := range routes { -// } -// } +func SplitRoutes(routes []*config.Route, receivers map[string]api.ReceiverType) (gRoutes, amRoutes []*config.Route, err error) { + for _, route := range routes { + var grafanaCt, amCt int + for _, receiver := range allReceivers(route) { + t, ok := receivers[receiver] + if !ok { + return nil, nil, fmt.Errorf("unexpected receiver (%s) is undefined", receiver) + } + + if t == api.GrafanaReceiverType { + grafanaCt++ + } else { + amCt++ + } + + } + + // TODO(owen-d): We may want to eventually support this, but short-circuiting for now + // to reduce routing tree complexity. + if grafanaCt > 0 && amCt > 0 { + return nil, nil, fmt.Errorf("cannot mix Grafana Managed receivers with Alertmanager receivers in the same (non-root) routing tree") + } + + if grafanaCt > 0 { + gRoutes = append(gRoutes, route) + } else { + amRoutes = append(amRoutes, route) + } + } + + return gRoutes, amRoutes, nil +} func allReceivers(route *config.Route) (res []string) { res = append(res, route.Receiver) diff --git a/pkg/validation/alertconfig_test.go b/pkg/validation/alertconfig_test.go index 48f4f624314..29f2d099e3c 100644 --- a/pkg/validation/alertconfig_test.go +++ b/pkg/validation/alertconfig_test.go @@ -3,6 +3,7 @@ package validation import ( "testing" + "github.com/grafana/alerting-api/pkg/api" "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/require" ) @@ -27,3 +28,129 @@ func Test_allReceivers(t *testing.T) { require.Equal(t, []string{"foo", "bar", "bazz", "buzz"}, allReceivers(input)) } + +func Test_Splitroutes(t *testing.T) { + receivers := map[string]api.ReceiverType{ + "am": api.AlertmanagerReceiverType, + "graf": api.GrafanaReceiverType, + } + + for _, tc := range []struct { + desc string + input []*config.Route + receivers map[string]api.ReceiverType + err bool + gRoutes, amRoutes []*config.Route + }{ + { + desc: "splits simple", + input: []*config.Route{ + { + Receiver: "am", + }, + { + Receiver: "graf", + }, + }, + err: false, + gRoutes: []*config.Route{ + { + Receiver: "graf", + }, + }, + amRoutes: []*config.Route{ + { + Receiver: "am", + }, + }, + }, + { + desc: "splits nested", + input: []*config.Route{ + { + Receiver: "am", + Routes: []*config.Route{ + { + Receiver: "am", + }, + }, + }, + { + Receiver: "graf", + Routes: []*config.Route{ + { + Receiver: "graf", + }, + }, + }, + }, + err: false, + gRoutes: []*config.Route{ + { + Receiver: "graf", + Routes: []*config.Route{ + { + Receiver: "graf", + }, + }, + }, + }, + amRoutes: []*config.Route{ + { + Receiver: "am", + Routes: []*config.Route{ + { + Receiver: "am", + }, + }, + }, + }, + }, + { + desc: "errors undefined receiver", + input: []*config.Route{ + { + Receiver: "am", + Routes: []*config.Route{ + { + Receiver: "unmentioned", + }, + }, + }, + }, + err: true, + }, + { + desc: "errors mixed", + input: []*config.Route{ + { + Receiver: "am", + Routes: []*config.Route{ + { + Receiver: "graf", + }, + }, + }, + { + Receiver: "graf", + Routes: []*config.Route{ + { + Receiver: "graf", + }, + }, + }, + }, + err: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + gRoutes, amRoutes, err := SplitRoutes(tc.input, receivers) + if tc.err { + require.Error(t, err) + } else { + require.Equal(t, tc.gRoutes, gRoutes) + require.Equal(t, tc.amRoutes, amRoutes) + } + }) + } +} From 0ef573754f372b5fec9d0689fdf0d2fd670f389c Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Wed, 24 Feb 2021 16:53:23 -0500 Subject: [PATCH 4/6] api marshaling & validation --- pkg/api/alertmanager.go | 59 +++++++++ pkg/api/alertmanager_test.go | 238 +++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) diff --git a/pkg/api/alertmanager.go b/pkg/api/alertmanager.go index 5177aeea43c..89f2cf63e4c 100644 --- a/pkg/api/alertmanager.go +++ b/pkg/api/alertmanager.go @@ -174,10 +174,68 @@ type AlertingConfigResponse struct { type ApiAlertingConfig struct { config.Config + // Override/eliminate Routes tree in AM config - we'll set it via new fields + Route struct{} `yaml:"route,omitempty" json:"route,omitempty"` + + 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 @@ -227,6 +285,7 @@ func (r *ApiReceiver) UnmarshalJSON(b []byte) error { } } + return nil } diff --git a/pkg/api/alertmanager_test.go b/pkg/api/alertmanager_test.go index 1a47516d581..a551e3775d3 100644 --- a/pkg/api/alertmanager_test.go +++ b/pkg/api/alertmanager_test.go @@ -63,3 +63,241 @@ func Test_ApiReceiver_Marshaling(t *testing.T) { }) } } + +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) + } + }) + } +} From 4ba7757281720d7b09ca83c8cf60a76e9ec937b7 Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Wed, 24 Feb 2021 17:06:55 -0500 Subject: [PATCH 5/6] begins work on routing lib --- pkg/routing/alertconfig.go | 48 +++++++++ pkg/validation/alertconfig.go | 64 ------------ pkg/validation/alertconfig_test.go | 156 ----------------------------- 3 files changed, 48 insertions(+), 220 deletions(-) create mode 100644 pkg/routing/alertconfig.go delete mode 100644 pkg/validation/alertconfig.go delete mode 100644 pkg/validation/alertconfig_test.go diff --git a/pkg/routing/alertconfig.go b/pkg/routing/alertconfig.go new file mode 100644 index 00000000000..45f486efffe --- /dev/null +++ b/pkg/routing/alertconfig.go @@ -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 +} diff --git a/pkg/validation/alertconfig.go b/pkg/validation/alertconfig.go deleted file mode 100644 index 1b73a722f40..00000000000 --- a/pkg/validation/alertconfig.go +++ /dev/null @@ -1,64 +0,0 @@ -package validation - -import ( - "fmt" - - "github.com/grafana/alerting-api/pkg/api" - "github.com/prometheus/alertmanager/config" -) - -// GrafanaAlertingConfig contains only the Grafana managed alerting configurations. -type GrafanaAlertingConfig struct { - api.GrafanaReceivers - Route *config.Route `yaml:"route,omitempty" json:"route,omitempty"` - Templates []string `yaml:"templates" json:"templates"` -} - -// func SplitAlertingConfig(apiConf api.ApiAlertingConfig) (amConfig config.Config, gConfig GrafanaAlertingConfig, err error) { - -// } - -// func MergeAlertingConfigs(amConfig config.Config, gConfig GrafanaAlertingConfig) (conf api.ApiAlertingConfig, err error) { -// } - -// Routing trees must either contain only Grafana Managed receiver types or AlertManager receiver types -func SplitRoutes(routes []*config.Route, receivers map[string]api.ReceiverType) (gRoutes, amRoutes []*config.Route, err error) { - for _, route := range routes { - var grafanaCt, amCt int - for _, receiver := range allReceivers(route) { - t, ok := receivers[receiver] - if !ok { - return nil, nil, fmt.Errorf("unexpected receiver (%s) is undefined", receiver) - } - - if t == api.GrafanaReceiverType { - grafanaCt++ - } else { - amCt++ - } - - } - - // TODO(owen-d): We may want to eventually support this, but short-circuiting for now - // to reduce routing tree complexity. - if grafanaCt > 0 && amCt > 0 { - return nil, nil, fmt.Errorf("cannot mix Grafana Managed receivers with Alertmanager receivers in the same (non-root) routing tree") - } - - if grafanaCt > 0 { - gRoutes = append(gRoutes, route) - } else { - amRoutes = append(amRoutes, route) - } - } - - return gRoutes, amRoutes, nil -} - -func allReceivers(route *config.Route) (res []string) { - res = append(res, route.Receiver) - for _, subRoute := range route.Routes { - res = append(res, allReceivers(subRoute)...) - } - return res -} diff --git a/pkg/validation/alertconfig_test.go b/pkg/validation/alertconfig_test.go deleted file mode 100644 index 29f2d099e3c..00000000000 --- a/pkg/validation/alertconfig_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package validation - -import ( - "testing" - - "github.com/grafana/alerting-api/pkg/api" - "github.com/prometheus/alertmanager/config" - "github.com/stretchr/testify/require" -) - -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_Splitroutes(t *testing.T) { - receivers := map[string]api.ReceiverType{ - "am": api.AlertmanagerReceiverType, - "graf": api.GrafanaReceiverType, - } - - for _, tc := range []struct { - desc string - input []*config.Route - receivers map[string]api.ReceiverType - err bool - gRoutes, amRoutes []*config.Route - }{ - { - desc: "splits simple", - input: []*config.Route{ - { - Receiver: "am", - }, - { - Receiver: "graf", - }, - }, - err: false, - gRoutes: []*config.Route{ - { - Receiver: "graf", - }, - }, - amRoutes: []*config.Route{ - { - Receiver: "am", - }, - }, - }, - { - desc: "splits nested", - input: []*config.Route{ - { - Receiver: "am", - Routes: []*config.Route{ - { - Receiver: "am", - }, - }, - }, - { - Receiver: "graf", - Routes: []*config.Route{ - { - Receiver: "graf", - }, - }, - }, - }, - err: false, - gRoutes: []*config.Route{ - { - Receiver: "graf", - Routes: []*config.Route{ - { - Receiver: "graf", - }, - }, - }, - }, - amRoutes: []*config.Route{ - { - Receiver: "am", - Routes: []*config.Route{ - { - Receiver: "am", - }, - }, - }, - }, - }, - { - desc: "errors undefined receiver", - input: []*config.Route{ - { - Receiver: "am", - Routes: []*config.Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - err: true, - }, - { - desc: "errors mixed", - input: []*config.Route{ - { - Receiver: "am", - Routes: []*config.Route{ - { - Receiver: "graf", - }, - }, - }, - { - Receiver: "graf", - Routes: []*config.Route{ - { - Receiver: "graf", - }, - }, - }, - }, - err: true, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - gRoutes, amRoutes, err := SplitRoutes(tc.input, receivers) - if tc.err { - require.Error(t, err) - } else { - require.Equal(t, tc.gRoutes, gRoutes) - require.Equal(t, tc.amRoutes, amRoutes) - } - }) - } -} From 9976efda9da6eaa128cb83e2fe50df72fc3be65e Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Wed, 24 Feb 2021 17:50:35 -0500 Subject: [PATCH 6/6] [hack] ignores embedded field in generation --- Makefile | 3 ++- README.md | 3 ++- pkg/api/alertmanager.go | 4 ++-- spec.json | 31 ++++++++++++++++++++++++------- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 86bb368b062..8ec0e88b4ea 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index f1a5f567006..b47529aff8c 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/pkg/api/alertmanager.go b/pkg/api/alertmanager.go index 89f2cf63e4c..3803f85812b 100644 --- a/pkg/api/alertmanager.go +++ b/pkg/api/alertmanager.go @@ -174,8 +174,8 @@ type AlertingConfigResponse struct { type ApiAlertingConfig struct { config.Config - // Override/eliminate Routes tree in AM config - we'll set it via new fields - Route struct{} `yaml:"route,omitempty" json:"route,omitempty"` + // 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"` diff --git a/spec.json b/spec.json index 78f7280390d..65ee2767eac 100644 --- a/spec.json +++ b/spec.json @@ -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" } } -} \ No newline at end of file +}