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 <josue.abreu@gmail.com>
---------

Signed-off-by: gotjosh <josue.abreu@gmail.com>
This commit is contained in:
gotjosh 2024-03-06 20:48:32 +00:00 committed by GitHub
parent d5fda06147
commit 948c8c45d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 60 additions and 2177 deletions

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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",

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",