From 948c8c45d686877db25a79424343b423a9574b65 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Wed, 6 Mar 2024 20:48:32 +0000 Subject: [PATCH] Alerting: Use Alertmanager types extracted into grafana/alerting (#83824) * Alerting: Use Alertmanager types extracted into grafana/alerting We're in the process of exporting all Alertmanager types into grafana/alerting so that they can be imported in the Mimir Alertmanager, without a neeed to import Grafana directly. This change introduces type aliasing for all Alertmanager types based on their 1:1 copy that now live in grafana/alerting. Signed-off-by: gotjosh --------- Signed-off-by: gotjosh --- go.mod | 2 +- go.sum | 4 +- pkg/services/ngalert/api/tooling/api.json | 385 +----- .../api/tooling/definitions/alertmanager.go | 612 +-------- .../tooling/definitions/alertmanager_test.go | 1104 ----------------- .../definitions/alertmanager_validation.go | 92 -- .../alertmanager_validation_test.go | 12 +- pkg/services/ngalert/api/tooling/post.json | 7 +- pkg/services/ngalert/api/tooling/spec.json | 7 +- public/api-merged.json | 6 + public/openapi3.json | 6 + 11 files changed, 60 insertions(+), 2177 deletions(-) diff --git a/go.mod b/go.mod index f03f6ef0461..c99e625f09e 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.6.0 // @grafana/backend-platform github.com/google/wire v0.5.0 // @grafana/backend-platform github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b // @grafana/alerting-squad-backend + github.com/grafana/alerting v0.0.0-20240306130925-bc622368256d // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.24.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index 4a635294b66..14762a4030a 100644 --- a/go.sum +++ b/go.sum @@ -2166,8 +2166,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b h1:rYx9ds94ZrueuXioEnoSqL737UYPSngPkMwBFl1guJE= -github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= +github.com/grafana/alerting v0.0.0-20240306130925-bc622368256d h1:YxLsj/C75sW90gzYK27XEaJ1sL89lYxuntmHaytFP80= +github.com/grafana/alerting v0.0.0-20240306130925-bc622368256d/go.mod h1:0nHKO0w8OTemvZ3eh7+s1EqGGhgbs0kvkTeLU1FrbTw= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index be0f9033b1e..9395f274c5d 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -398,9 +398,6 @@ }, "type": "object" }, - "AlertStateType": { - "type": "string" - }, "AlertingFileExport": { "properties": { "apiVersion": { @@ -528,80 +525,6 @@ }, "type": "object" }, - "Annotation": { - "properties": { - "alertId": { - "format": "int64", - "type": "integer" - }, - "alertName": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - }, - "created": { - "format": "int64", - "type": "integer" - }, - "dashboardId": { - "format": "int64", - "type": "integer" - }, - "dashboardUID": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Json" - }, - "email": { - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - }, - "login": { - "type": "string" - }, - "newState": { - "type": "string" - }, - "panelId": { - "format": "int64", - "type": "integer" - }, - "prevState": { - "type": "string" - }, - "tags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "text": { - "type": "string" - }, - "time": { - "format": "int64", - "type": "integer" - }, - "timeEnd": { - "format": "int64", - "type": "integer" - }, - "updated": { - "format": "int64", - "type": "integer" - }, - "userId": { - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, "ApiRuleNode": { "properties": { "alert": { @@ -816,75 +739,12 @@ }, "type": "array" }, - "CookieType": { - "type": "string" - }, "CounterResetHint": { "description": "or alternatively that we are dealing with a gauge histogram, where counter resets do not apply.", "format": "uint8", "title": "CounterResetHint contains the known information about a counter reset,", "type": "integer" }, - "CreateLibraryElementCommand": { - "description": "CreateLibraryElementCommand is the command for adding a LibraryElement", - "properties": { - "folderId": { - "description": "ID of the folder where the library element is stored.\n\nDeprecated: use FolderUID instead", - "format": "int64", - "type": "integer" - }, - "folderUid": { - "description": "UID of the folder where the library element is stored.", - "type": "string" - }, - "kind": { - "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", - "enum": [ - 1, - 2 - ], - "format": "int64", - "type": "integer" - }, - "model": { - "description": "The JSON model for the library element.", - "type": "object" - }, - "name": { - "description": "Name of the library element.", - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "type": "object" - }, - "DashboardACLUpdateItem": { - "properties": { - "permission": { - "$ref": "#/definitions/PermissionType" - }, - "role": { - "enum": [ - "None", - "Viewer", - "Editor", - "Admin" - ], - "type": "string" - }, - "teamId": { - "format": "int64", - "type": "integer" - }, - "userId": { - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, "DashboardUpgrade": { "properties": { "dashboardId": { @@ -2327,48 +2187,6 @@ }, "type": "array" }, - "MetricRequest": { - "properties": { - "debug": { - "type": "boolean" - }, - "from": { - "description": "From Start time in epoch timestamps in milliseconds or relative using Grafana time units.", - "example": "now-1h", - "type": "string" - }, - "queries": { - "description": "queries.refId – Specifies an identifier of the query. Is optional and default to “A”.\nqueries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId.\nqueries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.\nqueries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.", - "example": [ - { - "datasource": { - "uid": "PD8C576611E62080A" - }, - "format": "table", - "intervalMs": 86400000, - "maxDataPoints": 1092, - "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", - "refId": "A" - } - ], - "items": { - "$ref": "#/definitions/Json" - }, - "type": "array" - }, - "to": { - "description": "To End time in epoch timestamps in milliseconds or relative using Grafana time units.", - "example": "now", - "type": "string" - } - }, - "required": [ - "from", - "to", - "queries" - ], - "type": "object" - }, "MultiStatus": { "type": "object" }, @@ -2420,24 +2238,6 @@ }, "type": "object" }, - "NewApiKeyResult": { - "properties": { - "id": { - "example": 1, - "format": "int64", - "type": "integer" - }, - "key": { - "example": "glsa_yscW25imSKJIuav8zF37RZmnbiDvB05G_fcaaf58a", - "type": "string" - }, - "name": { - "example": "grafana", - "type": "string" - } - }, - "type": "object" - }, "NotFound": { "type": "object" }, @@ -2841,76 +2641,9 @@ }, "type": "object" }, - "PatchPrefsCmd": { - "properties": { - "cookies": { - "items": { - "$ref": "#/definitions/CookieType" - }, - "type": "array" - }, - "homeDashboardId": { - "default": 0, - "description": "The numerical :id of a favorited dashboard", - "format": "int64", - "type": "integer" - }, - "homeDashboardUID": { - "type": "string" - }, - "language": { - "type": "string" - }, - "queryHistory": { - "$ref": "#/definitions/QueryHistoryPreference" - }, - "theme": { - "enum": [ - "light", - "dark" - ], - "type": "string" - }, - "timezone": { - "enum": [ - "utc", - "browser" - ], - "type": "string" - }, - "weekStart": { - "type": "string" - } - }, - "type": "object" - }, - "Permission": { - "properties": { - "action": { - "type": "string" - }, - "created": { - "format": "date-time", - "type": "string" - }, - "scope": { - "type": "string" - }, - "updated": { - "format": "date-time", - "type": "string" - } - }, - "title": "Permission is the model for access control permissions.", - "type": "object" - }, "PermissionDenied": { "type": "object" }, - "PermissionType": { - "format": "int64", - "type": "integer" - }, "PostableApiAlertingConfig": { "properties": { "global": { @@ -3499,14 +3232,6 @@ }, "type": "object" }, - "QueryHistoryPreference": { - "properties": { - "homeTab": { - "type": "string" - } - }, - "type": "object" - }, "QueryStat": { "description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).", "properties": { @@ -3741,53 +3466,6 @@ "title": "Responses is a map of RefIDs (Unique Query ID) to DataResponses.", "type": "object" }, - "RoleDTO": { - "properties": { - "created": { - "format": "date-time", - "type": "string" - }, - "delegatable": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "global": { - "type": "boolean" - }, - "group": { - "type": "string" - }, - "hidden": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "permissions": { - "items": { - "$ref": "#/definitions/Permission" - }, - "type": "array" - }, - "uid": { - "type": "string" - }, - "updated": { - "format": "date-time", - "type": "string" - }, - "version": { - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, "Route": { "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { @@ -4676,6 +4354,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4711,62 +4390,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", - "type": "object" - }, - "UpdateDashboardACLCommand": { - "properties": { - "items": { - "items": { - "$ref": "#/definitions/DashboardACLUpdateItem" - }, - "type": "array" - } - }, - "type": "object" - }, - "UpdatePrefsCmd": { - "properties": { - "cookies": { - "items": { - "$ref": "#/definitions/CookieType" - }, - "type": "array" - }, - "homeDashboardId": { - "default": 0, - "description": "The numerical :id of a favorited dashboard", - "format": "int64", - "type": "integer" - }, - "homeDashboardUID": { - "type": "string" - }, - "language": { - "type": "string" - }, - "queryHistory": { - "$ref": "#/definitions/QueryHistoryPreference" - }, - "theme": { - "enum": [ - "light", - "dark", - "system" - ], - "type": "string" - }, - "timezone": { - "enum": [ - "utc", - "browser" - ], - "type": "string" - }, - "weekStart": { - "type": "string" - } - }, + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4972,6 +4596,7 @@ "type": "object" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -4995,6 +4620,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -5099,6 +4725,7 @@ "type": "object" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -5161,6 +4788,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -5215,6 +4843,7 @@ "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", diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 438d1f85f9b..3a9b6cc09b4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -4,15 +4,12 @@ import ( "context" "encoding/json" "fmt" - "reflect" - "sort" - "strings" "time" "github.com/go-openapi/strfmt" + "github.com/grafana/alerting/definition" amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" "gopkg.in/yaml.v3" ) @@ -246,6 +243,31 @@ import ( // 400: ValidationError // 404: NotFound +// Alias all the needed Alertmanager types, functions and constants so that they can be imported directly from grafana/alerting +// without having to modify any of the usage within Grafana. +type ( + Config = definition.Config + Route = definition.Route + PostableGrafanaReceiver = definition.PostableGrafanaReceiver + PostableApiAlertingConfig = definition.PostableApiAlertingConfig + RawMessage = definition.RawMessage + Provenance = definition.Provenance + ObjectMatchers = definition.ObjectMatchers + PostableApiReceiver = definition.PostableApiReceiver + PostableGrafanaReceivers = definition.PostableGrafanaReceivers + ReceiverType = definition.ReceiverType +) + +const ( + GrafanaReceiverType = definition.GrafanaReceiverType + AlertmanagerReceiverType = definition.AlertmanagerReceiverType +) + +var ( + AsGrafanaRoute = definition.AsGrafanaRoute + AllReceivers = definition.AllReceivers +) + // swagger:model type PermissionDenied struct{} @@ -646,8 +668,6 @@ func (c *PostableUserConfig) UnmarshalYAML(value *yaml.Node) error { return nil } -type Provenance string - // swagger:model type GettableUserConfig struct { TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` @@ -800,360 +820,6 @@ func (c *GettableApiAlertingConfig) validate() error { return nil } -// Config is the top-level configuration for Alertmanager's config files. -type Config struct { - Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` - Route *Route `yaml:"route,omitempty" json:"route,omitempty"` - InhibitRules []config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` - // MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0. - MuteTimeIntervals []config.MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` - // Templates is unused by Grafana Managed AM but is passed-through for compatibility with some external AMs. - Templates []string `yaml:"templates" json:"templates"` -} - -// A Route is a node that contains definitions of how to handle alerts. This is modified -// from the upstream alertmanager in that it adds the ObjectMatchers property. -type Route struct { - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` - - GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` - GroupBy []model.LabelName `yaml:"-" json:"-"` - GroupByAll bool `yaml:"-" json:"-"` - // 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" json:"continue,omitempty"` - Routes []*Route `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"` - - Provenance Provenance `yaml:"provenance,omitempty" json:"provenance,omitempty"` -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface for Route. This is a copy of alertmanager's upstream except it removes validation on the label key. -func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { - type plain Route - if err := unmarshal((*plain)(r)); err != nil { - return err - } - - return r.validateChild() -} - -// AsAMRoute returns an Alertmanager route from a Grafana route. The ObjectMatchers are converted to Matchers. -func (r *Route) AsAMRoute() *config.Route { - amRoute := &config.Route{ - Receiver: r.Receiver, - GroupByStr: r.GroupByStr, - GroupBy: r.GroupBy, - GroupByAll: r.GroupByAll, - Match: r.Match, - MatchRE: r.MatchRE, - Matchers: append(r.Matchers, r.ObjectMatchers...), - MuteTimeIntervals: r.MuteTimeIntervals, - Continue: r.Continue, - - GroupWait: r.GroupWait, - GroupInterval: r.GroupInterval, - RepeatInterval: r.RepeatInterval, - - Routes: make([]*config.Route, 0, len(r.Routes)), - } - for _, rt := range r.Routes { - amRoute.Routes = append(amRoute.Routes, rt.AsAMRoute()) - } - - return amRoute -} - -// AsGrafanaRoute returns a Grafana route from an Alertmanager route. The Matchers are converted to ObjectMatchers. -func AsGrafanaRoute(r *config.Route) *Route { - gRoute := &Route{ - Receiver: r.Receiver, - GroupByStr: r.GroupByStr, - GroupBy: r.GroupBy, - GroupByAll: r.GroupByAll, - Match: r.Match, - MatchRE: r.MatchRE, - ObjectMatchers: ObjectMatchers(r.Matchers), - MuteTimeIntervals: r.MuteTimeIntervals, - Continue: r.Continue, - - GroupWait: r.GroupWait, - GroupInterval: r.GroupInterval, - RepeatInterval: r.RepeatInterval, - - Routes: make([]*Route, 0, len(r.Routes)), - } - for _, rt := range r.Routes { - gRoute.Routes = append(gRoute.Routes, AsGrafanaRoute(rt)) - } - - return gRoute -} - -func (r *Route) ResourceType() string { - return "route" -} - -func (r *Route) ResourceID() string { - return "" -} - -// Config is the entrypoint for the embedded Alertmanager config with the exception of receivers. -// Prometheus historically uses yaml files as the method of configuration and thus some -// post-validation is included in the UnmarshalYAML method. Here we simply run this with -// a noop unmarshaling function in order to benefit from said validation. -func (c *Config) UnmarshalJSON(b []byte) error { - type plain Config - if err := json.Unmarshal(b, (*plain)(c)); err != nil { - return err - } - - noopUnmarshal := func(_ interface{}) error { return nil } - - if c.Global != nil { - if err := c.Global.UnmarshalYAML(noopUnmarshal); err != nil { - return err - } - } - - if c.Route == nil { - return fmt.Errorf("no routes provided") - } - - err := c.Route.Validate() - if err != nil { - return err - } - - for _, r := range c.InhibitRules { - if err := r.UnmarshalYAML(noopUnmarshal); err != nil { - return err - } - } - - tiNames := make(map[string]struct{}) - for _, mt := range c.MuteTimeIntervals { - if mt.Name == "" { - return fmt.Errorf("missing name in mute time interval") - } - if _, ok := tiNames[mt.Name]; ok { - return fmt.Errorf("mute time interval %q is not unique", mt.Name) - } - tiNames[mt.Name] = struct{}{} - } - for _, ti := range c.TimeIntervals { - if ti.Name == "" { - return fmt.Errorf("missing name in time interval") - } - if _, ok := tiNames[ti.Name]; ok { - return fmt.Errorf("time interval %q is not unique", ti.Name) - } - tiNames[ti.Name] = struct{}{} - } - return checkTimeInterval(c.Route, tiNames) -} - -func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { - for _, sr := range r.Routes { - if err := checkTimeInterval(sr, timeIntervals); err != nil { - return err - } - } - if len(r.MuteTimeIntervals) == 0 { - return nil - } - for _, mt := range r.MuteTimeIntervals { - if _, ok := timeIntervals[mt]; !ok { - return fmt.Errorf("undefined time interval %q used in route", mt) - } - } - return nil -} - -type PostableApiAlertingConfig struct { - Config `yaml:",inline"` - - // Override with our superset receiver type - Receivers []*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` -} - -func (c *PostableApiAlertingConfig) GetReceivers() []*PostableApiReceiver { - return c.Receivers -} - -func (c *PostableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { - return c.MuteTimeIntervals -} - -func (c *PostableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } - -func (c *PostableApiAlertingConfig) GetRoute() *Route { - return c.Route -} - -func (c *PostableApiAlertingConfig) UnmarshalJSON(b []byte) error { - type plain PostableApiAlertingConfig - if err := json.Unmarshal(b, (*plain)(c)); err != nil { - return err - } - - // Since Config implements json.Unmarshaler, we must handle _all_ other fields independently. - // Otherwise, the json decoder will detect this and only use the embedded type. - // Additionally, we'll use pointers to slices in order to reference the intended target. - type overrides struct { - Receivers *[]*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` - } - - if err := json.Unmarshal(b, &overrides{Receivers: &c.Receivers}); err != nil { - return err - } - - return c.validate() -} - -// validate ensures that the two routing trees use the correct receiver types. -func (c *PostableApiAlertingConfig) validate() error { - receivers := make(map[string]struct{}, len(c.Receivers)) - - var hasGrafReceivers, hasAMReceivers bool - for _, r := range c.Receivers { - receivers[r.Name] = struct{}{} - switch r.Type() { - case GrafanaReceiverType: - hasGrafReceivers = true - case AlertmanagerReceiverType: - hasAMReceivers = true - default: - continue - } - } - - if hasGrafReceivers && hasAMReceivers { - return fmt.Errorf("cannot mix Alertmanager & Grafana receiver types") - } - - if hasGrafReceivers { - // Taken from https://github.com/prometheus/alertmanager/blob/master/config/config.go#L170-L191 - // Check if we have a root route. We cannot check for it in the - // UnmarshalYAML method because it won't be called if the input is empty - // (e.g. the config file is empty or only contains whitespace). - if c.Route == nil { - return fmt.Errorf("no route provided in config") - } - - // Check if continue in root route. - if c.Route.Continue { - return fmt.Errorf("cannot have continue in root route") - } - } - - for _, receiver := range AllReceivers(c.Route.AsAMRoute()) { - _, ok := receivers[receiver] - if !ok { - return fmt.Errorf("unexpected receiver (%s) is undefined", receiver) - } - } - - return nil -} - -// Type requires validate has been called and just checks the first receiver type -func (c *PostableApiAlertingConfig) ReceiverType() ReceiverType { - for _, r := range c.Receivers { - switch r.Type() { - case GrafanaReceiverType: - return GrafanaReceiverType - case AlertmanagerReceiverType: - return AlertmanagerReceiverType - default: - continue - } - } - return EmptyReceiverType -} - -// AllReceivers will recursively walk a routing tree and return a list of all the -// referenced receiver names. -func AllReceivers(route *config.Route) (res []string) { - if route == nil { - return res - } - // TODO: Consider removing this check when new resource-specific AM APIs are implemented. - // Skip autogenerated routes. This helps cover the case where an admin POSTs the autogenerated route back to us. - // For example, when deleting a contact point that is unused but still referenced in the autogenerated route. - if isAutogeneratedRoot(route) { - return nil - } - - if route.Receiver != "" { - res = append(res, route.Receiver) - } - - for _, subRoute := range route.Routes { - res = append(res, AllReceivers(subRoute)...) - } - return res -} - -// autogeneratedRouteLabel a label name used to distinguish alerts that are supposed to be handled by the autogenerated policy. Only expected value is `true`. -const autogeneratedRouteLabel = "__grafana_autogenerated__" - -// isAutogeneratedRoot returns true if the route is the root of an autogenerated route. -func isAutogeneratedRoot(route *config.Route) bool { - return len(route.Matchers) == 1 && route.Matchers[0].Name == autogeneratedRouteLabel -} - -type RawMessage json.RawMessage // This type alias adds YAML marshaling to the json.RawMessage. - -// MarshalJSON returns m as the JSON encoding of m. -func (r RawMessage) MarshalJSON() ([]byte, error) { - return json.Marshal(json.RawMessage(r)) -} - -func (r *RawMessage) UnmarshalJSON(data []byte) error { - var raw json.RawMessage - err := json.Unmarshal(data, &raw) - if err != nil { - return err - } - *r = RawMessage(raw) - return nil -} - -func (r *RawMessage) UnmarshalYAML(unmarshal func(interface{}) error) error { - var data interface{} - if err := unmarshal(&data); err != nil { - return err - } - bytes, err := json.Marshal(data) - if err != nil { - return err - } - *r = bytes - return nil -} - -func (r RawMessage) MarshalYAML() (interface{}, error) { - if r == nil { - return nil, nil - } - var d interface{} - err := json.Unmarshal(r, &d) - if err != nil { - return nil, err - } - return d, nil -} - type GettableGrafanaReceiver struct { UID string `json:"uid"` Name string `json:"name"` @@ -1164,41 +830,6 @@ type GettableGrafanaReceiver struct { Provenance Provenance `json:"provenance,omitempty"` } -type PostableGrafanaReceiver struct { - UID string `json:"uid"` - Name string `json:"name"` - Type string `json:"type"` - DisableResolveMessage bool `json:"disableResolveMessage"` - Settings RawMessage `json:"settings,omitempty"` - SecureSettings map[string]string `json:"secureSettings"` -} - -type ReceiverType int - -const ( - GrafanaReceiverType ReceiverType = 1 << iota - 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 } - type GettableApiReceiver struct { config.Receiver `yaml:",inline"` GettableGrafanaReceivers `yaml:",inline"` @@ -1253,199 +884,8 @@ func (r *GettableApiReceiver) GetName() string { return r.Receiver.Name } -type PostableApiReceiver struct { - config.Receiver `yaml:",inline"` - PostableGrafanaReceivers `yaml:",inline"` -} - -func (r *PostableApiReceiver) UnmarshalYAML(unmarshal func(interface{}) error) error { - if err := unmarshal(&r.PostableGrafanaReceivers); err != nil { - return err - } - - if err := unmarshal(&r.Receiver); err != nil { - return err - } - - return nil -} - -func (r *PostableApiReceiver) UnmarshalJSON(b []byte) error { - type plain PostableApiReceiver - if err := json.Unmarshal(b, (*plain)(r)); err != nil { - return err - } - - hasGrafanaReceivers := len(r.PostableGrafanaReceivers.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 *PostableApiReceiver) Type() ReceiverType { - if len(r.PostableGrafanaReceivers.GrafanaManagedReceivers) > 0 { - return GrafanaReceiverType - } - - cpy := r.Receiver - cpy.Name = "" - if reflect.ValueOf(cpy).IsZero() { - return EmptyReceiverType - } - - return AlertmanagerReceiverType -} - -func (r *PostableApiReceiver) GetName() string { - return r.Receiver.Name -} - type GettableGrafanaReceivers struct { GrafanaManagedReceivers []*GettableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"` } -type PostableGrafanaReceivers struct { - GrafanaManagedReceivers []*PostableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"` -} - type EncryptFn func(ctx context.Context, payload []byte) ([]byte, error) - -// ObjectMatcher is a matcher that can be used to filter alerts. -// swagger:model ObjectMatcher -type ObjectMatcherAPIModel [3]string - -// ObjectMatchers is a list of matchers that can be used to filter alerts. -// swagger:model ObjectMatchers -type ObjectMatchersAPIModel []ObjectMatcherAPIModel - -// swagger:ignore -// ObjectMatchers is Matchers with a different Unmarshal and Marshal methods that accept matchers as objects -// that have already been parsed. -type ObjectMatchers labels.Matchers - -// UnmarshalYAML implements the yaml.Unmarshaler interface for Matchers. -func (m *ObjectMatchers) UnmarshalYAML(unmarshal func(interface{}) error) error { - var rawMatchers ObjectMatchersAPIModel - if err := unmarshal(&rawMatchers); err != nil { - return err - } - for _, rawMatcher := range rawMatchers { - var matchType labels.MatchType - switch rawMatcher[1] { - case "=": - matchType = labels.MatchEqual - case "!=": - matchType = labels.MatchNotEqual - case "=~": - matchType = labels.MatchRegexp - case "!~": - matchType = labels.MatchNotRegexp - default: - return fmt.Errorf("unsupported match type %q in matcher", rawMatcher[1]) - } - - // When Prometheus serializes a matcher, the value gets wrapped in quotes: - // https://github.com/prometheus/alertmanager/blob/main/pkg/labels/matcher.go#L77 - // Remove these quotes so that we are matching against the right value. - // - // This is a stop-gap solution which will be superceded by https://github.com/grafana/grafana/issues/50040. - // - // The ngalert migration converts matchers into the Prom-style, quotes included. - // The UI then stores the quotes into ObjectMatchers without removing them. - // This approach allows these extra quotes to be stored in the database, and fixes them at read time. - // This works because the database stores matchers as JSON text. - // - // There is a subtle bug here, where users might intentionally add quotes to matchers. This method can remove such quotes. - // Since ObjectMatchers will be deprecated entirely, this bug will go away naturally with time. - rawMatcher[2] = strings.TrimPrefix(rawMatcher[2], "\"") - rawMatcher[2] = strings.TrimSuffix(rawMatcher[2], "\"") - - matcher, err := labels.NewMatcher(matchType, rawMatcher[0], rawMatcher[2]) - if err != nil { - return err - } - *m = append(*m, matcher) - } - sort.Sort(labels.Matchers(*m)) - return nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface for Matchers. -func (m *ObjectMatchers) UnmarshalJSON(data []byte) error { - var rawMatchers ObjectMatchersAPIModel - if err := json.Unmarshal(data, &rawMatchers); err != nil { - return err - } - for _, rawMatcher := range rawMatchers { - var matchType labels.MatchType - switch rawMatcher[1] { - case "=": - matchType = labels.MatchEqual - case "!=": - matchType = labels.MatchNotEqual - case "=~": - matchType = labels.MatchRegexp - case "!~": - matchType = labels.MatchNotRegexp - default: - return fmt.Errorf("unsupported match type %q in matcher", rawMatcher[1]) - } - - rawMatcher[2] = strings.TrimPrefix(rawMatcher[2], "\"") - rawMatcher[2] = strings.TrimSuffix(rawMatcher[2], "\"") - - matcher, err := labels.NewMatcher(matchType, rawMatcher[0], rawMatcher[2]) - if err != nil { - return err - } - *m = append(*m, matcher) - } - sort.Sort(labels.Matchers(*m)) - return nil -} - -// MarshalYAML implements the yaml.Marshaler interface for Matchers. -func (m ObjectMatchers) MarshalYAML() (interface{}, error) { - result := make(ObjectMatchersAPIModel, len(m)) - for i, matcher := range m { - result[i] = ObjectMatcherAPIModel{matcher.Name, matcher.Type.String(), matcher.Value} - } - return result, nil -} - -// MarshalJSON implements the json.Marshaler interface for Matchers. -func (m ObjectMatchers) MarshalJSON() ([]byte, error) { - if len(m) == 0 { - return nil, nil - } - result := make(ObjectMatchersAPIModel, len(m)) - for i, matcher := range m { - result[i] = ObjectMatcherAPIModel{matcher.Name, matcher.Type.String(), matcher.Value} - } - return json.Marshal(result) -} diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index 4f0504e350a..55821b9ba58 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -2,920 +2,17 @@ package definitions import ( "encoding/json" - "errors" "os" "strings" "testing" "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) -func Test_ApiReceiver_Marshaling(t *testing.T) { - for _, tc := range []struct { - desc string - input PostableApiReceiver - err bool - }{ - { - desc: "success AM", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - { - desc: "success GM", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - { - desc: "failure mixed", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - err: true, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - encoded, err := json.Marshal(tc.input) - require.Nil(t, err) - - var out PostableApiReceiver - err = json.Unmarshal(encoded, &out) - - if tc.err { - require.Error(t, err) - } else { - require.Nil(t, err) - require.Equal(t, tc.input, out) - } - }) - } -} - -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) { - input := &Route{ - Receiver: "foo", - Routes: []*Route{ - { - Receiver: "bar", - Routes: []*Route{ - { - Receiver: "bazz", - }, - }, - }, - { - Receiver: "buzz", - }, - }, - } - - require.Equal(t, []string{"foo", "bar", "bazz", "buzz"}, AllReceivers(input.AsAMRoute())) - - // test empty - var empty []string - emptyRoute := &Route{} - require.Equal(t, empty, AllReceivers(emptyRoute.AsAMRoute())) -} - -func Test_ApiAlertingConfig_Marshaling(t *testing.T) { - for _, tc := range []struct { - desc string - input PostableApiAlertingConfig - err bool - }{ - { - desc: "success am", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "am", - Routes: []*Route{ - { - Receiver: "am", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - }, - }, - { - desc: "success graf", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "graf", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - }, - { - desc: "failure undefined am receiver", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "am", - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure undefined graf receiver", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf no route", - input: PostableApiAlertingConfig{ - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf no default receiver", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Routes: []*Route{ - { - Receiver: "graf", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf root route with matchers", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "graf", - }, - }, - Match: map[string]string{"foo": "bar"}, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf nested route duplicate group by labels", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "graf", - GroupByStr: []string{"foo", "bar", "foo"}, - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "success undefined am receiver in autogenerated route is ignored", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "am", - Routes: []*Route{ - { - Matchers: config.Matchers{ - { - Name: autogeneratedRouteLabel, - Type: labels.MatchEqual, - Value: "true", - }, - }, - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - }, - err: false, - }, - { - desc: "success undefined graf receiver in autogenerated route is ignored", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Matchers: config.Matchers{ - { - Name: autogeneratedRouteLabel, - Type: labels.MatchEqual, - Value: "true", - }, - }, - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: false, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - encoded, err := json.Marshal(tc.input) - require.Nil(t, err) - - var out PostableApiAlertingConfig - err = json.Unmarshal(encoded, &out) - - if tc.err { - require.Error(t, err) - } else { - require.Nil(t, err) - require.Equal(t, tc.input, out) - } - }) - } -} - -func Test_PostableApiReceiver_Unmarshaling_YAML(t *testing.T) { - for _, tc := range []struct { - desc string - input string - rtype ReceiverType - }{ - { - desc: "grafana receivers", - input: ` -name: grafana_managed -grafana_managed_receiver_configs: - - uid: alertmanager UID - name: an alert manager receiver - type: prometheus-alertmanager - sendreminder: false - disableresolvemessage: false - frequency: 5m - isdefault: false - settings: {} - securesettings: - basicAuthPassword: - - uid: dingding UID - name: a dingding receiver - type: dingding - sendreminder: false - disableresolvemessage: false - frequency: 5m - isdefault: false`, - rtype: GrafanaReceiverType, - }, - { - desc: "receiver", - input: ` -name: example-email -email_configs: - - to: 'youraddress@example.org'`, - rtype: AlertmanagerReceiverType, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - var r PostableApiReceiver - err := yaml.Unmarshal([]byte(tc.input), &r) - require.Nil(t, err) - assert.Equal(t, tc.rtype, r.Type()) - }) - } -} - -func Test_ConfigUnmashaling(t *testing.T) { - for _, tc := range []struct { - desc, input string - err error - }{ - { - desc: "missing mute time interval name should error", - err: errors.New("missing name in mute time interval"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "mute_time_intervals": [ - { - "name": "", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "missing time interval name should error", - err: errors.New("missing name in time interval"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "time_intervals": [ - { - "name": "", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "duplicate mute time interval names should error", - err: errors.New("mute time interval \"test1\" is not unique"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - }, - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "duplicate time interval names should error", - err: errors.New("time interval \"test1\" is not unique"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - }, - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "duplicate time and mute time interval names should error", - err: errors.New("time interval \"test1\" is not unique"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "mute time intervals on root route should error", - err: errors.New("root route must not have any mute time intervals"), - input: ` - { - "route": { - "receiver": "grafana-default-email", - "mute_time_intervals": ["test1"] - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "undefined mute time names in routes should error", - err: errors.New("undefined time interval \"test2\" used in route"), - input: ` - { - "route": { - "receiver": "grafana-default-email", - "routes": [ - { - "receiver": "grafana-default-email", - "object_matchers": [ - [ - "a", - "=", - "b" - ] - ], - "mute_time_intervals": [ - "test2" - ] - } - ] - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "valid config should not error", - input: ` - { - "route": { - "receiver": "grafana-default-email", - "routes": [ - { - "receiver": "grafana-default-email", - "object_matchers": [ - [ - "a", - "=", - "b" - ] - ], - "mute_time_intervals": [ - "test1" - ] - } - ] - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - var out Config - err := json.Unmarshal([]byte(tc.input), &out) - require.Equal(t, tc.err, err) - }) - } -} - func Test_GettableUserConfigUnmarshaling(t *testing.T) { for _, tc := range []struct { desc, input string @@ -1062,207 +159,6 @@ func Test_GettableUserConfigRoundtrip(t *testing.T) { 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 ReceiverType - ok bool - }{ - { - desc: "graf=graf", - rec: GrafanaReceiverType, - b: GrafanaReceiverType, - ok: true, - }, - { - desc: "empty=graf", - rec: EmptyReceiverType, - b: GrafanaReceiverType, - ok: true, - }, - { - desc: "am=am", - rec: AlertmanagerReceiverType, - b: AlertmanagerReceiverType, - ok: true, - }, - { - desc: "empty=am", - rec: EmptyReceiverType, - b: AlertmanagerReceiverType, - ok: true, - }, - { - desc: "graf!=am", - rec: GrafanaReceiverType, - b: AlertmanagerReceiverType, - ok: false, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - ok := tc.rec.Can(tc.b) - require.Equal(t, tc.ok, ok) - }) - } -} - -func TestObjectMatchers_UnmarshalJSON(t *testing.T) { - j := `{ - "receiver": "autogen-contact-point-default", - "routes": [{ - "receiver": "autogen-contact-point-1", - "object_matchers": [ - [ - "a", - "=", - "MFR3Gxrnk" - ], - [ - "b", - "=", - "\"MFR3Gxrnk\"" - ], - [ - "c", - "=~", - "^[a-z0-9-]{1}[a-z0-9-]{0,30}$" - ], - [ - "d", - "=~", - "\"^[a-z0-9-]{1}[a-z0-9-]{0,30}$\"" - ] - ], - "group_interval": "3s", - "repeat_interval": "10s" - }] -}` - var r Route - if err := json.Unmarshal([]byte(j), &r); err != nil { - require.NoError(t, err) - } - - matchers := r.Routes[0].ObjectMatchers - - // Without quotes. - require.Equal(t, matchers[0].Name, "a") - require.Equal(t, matchers[0].Value, "MFR3Gxrnk") - - // With double quotes. - require.Equal(t, matchers[1].Name, "b") - require.Equal(t, matchers[1].Value, "MFR3Gxrnk") - - // Regexp without quotes. - require.Equal(t, matchers[2].Name, "c") - require.Equal(t, matchers[2].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") - - // Regexp with quotes. - require.Equal(t, matchers[3].Name, "d") - require.Equal(t, matchers[3].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") -} - -func TestObjectMatchers_UnmarshalYAML(t *testing.T) { - y := `--- -receiver: autogen-contact-point-default -routes: -- receiver: autogen-contact-point-1 - object_matchers: - - - a - - "=" - - MFR3Gxrnk - - - b - - "=" - - '"MFR3Gxrnk"' - - - c - - "=~" - - "^[a-z0-9-]{1}[a-z0-9-]{0,30}$" - - - d - - "=~" - - '"^[a-z0-9-]{1}[a-z0-9-]{0,30}$"' - group_interval: 3s - repeat_interval: 10s -` - - var r Route - if err := yaml.Unmarshal([]byte(y), &r); err != nil { - require.NoError(t, err) - } - - matchers := r.Routes[0].ObjectMatchers - - // Without quotes. - require.Equal(t, matchers[0].Name, "a") - require.Equal(t, matchers[0].Value, "MFR3Gxrnk") - - // With double quotes. - require.Equal(t, matchers[1].Name, "b") - require.Equal(t, matchers[1].Value, "MFR3Gxrnk") - - // Regexp without quotes. - require.Equal(t, matchers[2].Name, "c") - require.Equal(t, matchers[2].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") - - // Regexp with quotes. - require.Equal(t, matchers[3].Name, "d") - require.Equal(t, matchers[3].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") -} - func Test_Marshaling_Validation(t *testing.T) { jsonEncoded, err := os.ReadFile("alertmanager_test_artifact.json") require.Nil(t, err) diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go index 9bec82b6d79..72ca0ebb730 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go @@ -6,61 +6,11 @@ import ( "regexp" "strings" tmpltext "text/template" - "time" "github.com/prometheus/alertmanager/template" - "github.com/prometheus/common/model" "gopkg.in/yaml.v3" ) -// groupByAll is a special value defined by alertmanager that can be used in a Route's GroupBy field to aggregate by all possible labels. -const groupByAll = "..." - -// Validate normalizes a possibly nested Route r, and returns errors if r is invalid. -func (r *Route) validateChild() error { - r.GroupBy = nil - r.GroupByAll = false - for _, l := range r.GroupByStr { - if l == groupByAll { - r.GroupByAll = true - } else { - r.GroupBy = append(r.GroupBy, model.LabelName(l)) - } - } - - if len(r.GroupBy) > 0 && r.GroupByAll { - return fmt.Errorf("cannot have wildcard group_by (`...`) and other other labels at the same time") - } - - groupBy := map[model.LabelName]struct{}{} - - for _, ln := range r.GroupBy { - if _, ok := groupBy[ln]; ok { - return fmt.Errorf("duplicated label %q in group_by, %s %s", ln, r.Receiver, r.GroupBy) - } - groupBy[ln] = struct{}{} - } - - if r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) { - return fmt.Errorf("group_interval cannot be zero") - } - if r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) { - return fmt.Errorf("repeat_interval cannot be zero") - } - - // Routes are a self-referential structure. - if r.Routes != nil { - for _, child := range r.Routes { - err := child.validateChild() - if err != nil { - return err - } - } - } - - return nil -} - func (t *NotificationTemplate) Validate() error { if t.Name == "" { return fmt.Errorf("template must have a name") @@ -102,48 +52,6 @@ func (t *NotificationTemplate) Validate() error { return nil } -// Validate normalizes a Route r, and returns errors if r is an invalid root route. Root routes must satisfy a few additional conditions. -func (r *Route) Validate() error { - if len(r.Receiver) == 0 { - return fmt.Errorf("root route must specify a default receiver") - } - if len(r.Match) > 0 || len(r.MatchRE) > 0 { - return fmt.Errorf("root route must not have any matchers") - } - if len(r.MuteTimeIntervals) > 0 { - return fmt.Errorf("root route must not have any mute time intervals") - } - return r.validateChild() -} - -func (r *Route) ValidateReceivers(receivers map[string]struct{}) error { - if _, exists := receivers[r.Receiver]; !exists { - return fmt.Errorf("receiver '%s' does not exist", r.Receiver) - } - for _, children := range r.Routes { - err := children.ValidateReceivers(receivers) - if err != nil { - return err - } - } - return nil -} - -func (r *Route) ValidateMuteTimes(muteTimes map[string]struct{}) error { - for _, name := range r.MuteTimeIntervals { - if _, exists := muteTimes[name]; !exists { - return fmt.Errorf("mute time interval '%s' does not exist", name) - } - } - for _, child := range r.Routes { - err := child.ValidateMuteTimes(muteTimes) - if err != nil { - return err - } - } - return nil -} - func (mt *MuteTimeInterval) Validate() error { s, err := yaml.Marshal(mt.MuteTimeInterval) if err != nil { diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go index cfb504b7291..4fbbd983b13 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go @@ -48,7 +48,7 @@ func TestValidateRoutes(t *testing.T) { for _, c := range cases { t.Run(c.desc, func(t *testing.T) { - err := c.route.validateChild() + err := c.route.ValidateChild() require.NoError(t, err) }) @@ -117,7 +117,7 @@ func TestValidateRoutes(t *testing.T) { for _, c := range cases { t.Run(c.desc, func(t *testing.T) { - err := c.route.validateChild() + err := c.route.ValidateChild() require.Error(t, err) require.Contains(t, err.Error(), c.expMsg) @@ -132,7 +132,7 @@ func TestValidateRoutes(t *testing.T) { GroupByStr: []string{"abc", "def"}, } - _ = route.validateChild() + _ = route.ValidateChild() require.False(t, route.GroupByAll) require.Equal(t, []model.LabelName{"abc", "def"}, route.GroupBy) @@ -144,7 +144,7 @@ func TestValidateRoutes(t *testing.T) { GroupByStr: []string{"..."}, } - _ = route.validateChild() + _ = route.ValidateChild() require.True(t, route.GroupByAll) require.Nil(t, route.GroupBy) @@ -156,9 +156,9 @@ func TestValidateRoutes(t *testing.T) { GroupByStr: []string{"abc", "def"}, } - err := route.validateChild() + err := route.ValidateChild() require.NoError(t, err) - err = route.validateChild() + err = route.ValidateChild() require.NoError(t, err) require.False(t, route.GroupByAll) diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 44b31d7c652..8c9e8a312f7 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -4354,6 +4354,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4389,7 +4390,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4619,7 +4620,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -4724,7 +4724,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -4787,6 +4786,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -4986,7 +4986,6 @@ "type": "array" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 90aae29c3e5..191253a57b8 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -7959,8 +7959,9 @@ } }, "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 the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "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" @@ -8225,7 +8226,6 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -8331,7 +8331,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -8396,6 +8395,7 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -8598,7 +8598,6 @@ } }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/public/api-merged.json b/public/api-merged.json index 40061884c2e..dd34a342523 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17045,6 +17045,7 @@ }, "ObjectMatchers": { "type": "array", + "title": "ObjectMatchers is a list of matchers that can be used to filter alerts.", "items": { "$ref": "#/definitions/ObjectMatcher" } @@ -21795,6 +21796,7 @@ } }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -21818,6 +21820,7 @@ } }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -21950,6 +21953,7 @@ } }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -22012,6 +22016,7 @@ } }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -22066,6 +22071,7 @@ } }, "integration": { + "description": "Integration integration", "type": "object", "required": [ "name", diff --git a/public/openapi3.json b/public/openapi3.json index 5291c6fe76f..d6f99d851f9 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7557,6 +7557,7 @@ "items": { "$ref": "#/components/schemas/ObjectMatcher" }, + "title": "ObjectMatchers is a list of matchers that can be used to filter alerts.", "type": "array" }, "OpsGenieConfig": { @@ -12304,6 +12305,7 @@ "type": "object" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -12327,6 +12329,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/components/schemas/alertGroup" }, @@ -12459,6 +12462,7 @@ "type": "object" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/components/schemas/labelSet" @@ -12521,6 +12525,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -12575,6 +12580,7 @@ "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",