Omit empty config fields and show regex upon re-marshalling to elide secrets (#864)
* Omit empty config fields upon remarshalling to elide secrets * added test checking for empty or null fields and blank regexps
This commit is contained in:
parent
6b5fb2dbc9
commit
b5ad65fa32
|
@ -303,19 +303,19 @@ type GlobalConfig struct {
|
|||
// if it has not been updated.
|
||||
ResolveTimeout model.Duration `yaml:"resolve_timeout" json:"resolve_timeout"`
|
||||
|
||||
SMTPFrom string `yaml:"smtp_from" json:"smtp_from"`
|
||||
SMTPSmarthost string `yaml:"smtp_smarthost" json:"smtp_smarthost"`
|
||||
SMTPAuthUsername string `yaml:"smtp_auth_username" json:"smtp_auth_username"`
|
||||
SMTPAuthPassword Secret `yaml:"smtp_auth_password" json:"smtp_auth_password"`
|
||||
SMTPAuthSecret Secret `yaml:"smtp_auth_secret" json:"smtp_auth_secret"`
|
||||
SMTPAuthIdentity string `yaml:"smtp_auth_identity" json:"smtp_auth_identity"`
|
||||
SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls"`
|
||||
SlackAPIURL Secret `yaml:"slack_api_url" json:"slack_api_url"`
|
||||
PagerdutyURL string `yaml:"pagerduty_url" json:"pagerduty_url"`
|
||||
HipchatURL string `yaml:"hipchat_url" json:"hipchat_url"`
|
||||
HipchatAuthToken Secret `yaml:"hipchat_auth_token" json:"hipchat_auth_token"`
|
||||
OpsGenieAPIHost string `yaml:"opsgenie_api_host" json:"opsgenie_api_host"`
|
||||
VictorOpsAPIURL string `yaml:"victorops_api_url" json:"victorops_api_url"`
|
||||
SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"`
|
||||
SMTPSmarthost string `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"`
|
||||
SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"`
|
||||
SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"`
|
||||
SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"`
|
||||
SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"`
|
||||
SMTPRequireTLS bool `yaml:"smtp_require_tls,omitempty" json:"smtp_require_tls,omitempty"`
|
||||
SlackAPIURL Secret `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"`
|
||||
PagerdutyURL string `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"`
|
||||
HipchatURL string `yaml:"hipchat_url,omitempty" json:"hipchat_url,omitempty"`
|
||||
HipchatAuthToken Secret `yaml:"hipchat_auth_token,omitempty" json:"hipchat_auth_token,omitempty"`
|
||||
OpsGenieAPIHost string `yaml:"opsgenie_api_host,omitempty" json:"opsgenie_api_host,omitempty"`
|
||||
VictorOpsAPIURL string `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
|
@ -386,19 +386,19 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
type InhibitRule struct {
|
||||
// SourceMatch defines a set of labels that have to equal the given
|
||||
// value for source alerts.
|
||||
SourceMatch map[string]string `yaml:"source_match" json:"source_match"`
|
||||
SourceMatch map[string]string `yaml:"source_match,omitempty" json:"source_match,omitempty"`
|
||||
// SourceMatchRE defines pairs like SourceMatch but does regular expression
|
||||
// matching.
|
||||
SourceMatchRE map[string]Regexp `yaml:"source_match_re" json:"source_match_re"`
|
||||
SourceMatchRE map[string]Regexp `yaml:"source_match_re,omitempty" json:"source_match_re,omitempty"`
|
||||
// TargetMatch defines a set of labels that have to equal the given
|
||||
// value for target alerts.
|
||||
TargetMatch map[string]string `yaml:"target_match" json:"target_match"`
|
||||
TargetMatch map[string]string `yaml:"target_match,omitempty" json:"target_match,omitempty"`
|
||||
// TargetMatchRE defines pairs like TargetMatch but does regular expression
|
||||
// matching.
|
||||
TargetMatchRE map[string]Regexp `yaml:"target_match_re" json:"target_match_re"`
|
||||
TargetMatchRE map[string]Regexp `yaml:"target_match_re,omitempty" json:"target_match_re,omitempty"`
|
||||
// A set of labels that must be equal between the source and target alert
|
||||
// for them to be a match.
|
||||
Equal model.LabelNames `yaml:"equal" json:"equal"`
|
||||
Equal model.LabelNames `yaml:"equal,omitempty" json:"equal,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
|
@ -488,8 +488,8 @@ func (re *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
}
|
||||
|
||||
// MarshalYAML implements the yaml.Marshaler interface.
|
||||
func (re *Regexp) MarshalYAML() (interface{}, error) {
|
||||
if re != nil {
|
||||
func (re Regexp) MarshalYAML() (interface{}, error) {
|
||||
if re.Regexp != nil {
|
||||
return re.String(), nil
|
||||
}
|
||||
return nil, nil
|
||||
|
|
|
@ -15,11 +15,13 @@ package config
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
@ -69,14 +71,13 @@ receivers:
|
|||
func TestHideConfigSecrets(t *testing.T) {
|
||||
c, _, err := LoadFile("testdata/conf.good.yml")
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %s: %s", "testdata/good.yml", err)
|
||||
t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err)
|
||||
}
|
||||
|
||||
// String method must not reveal authentication credentials.
|
||||
s := c.String()
|
||||
secretRe := regexp.MustCompile("<secret>")
|
||||
matches := secretRe.FindAllStringIndex(s, -1)
|
||||
fmt.Println(len(matches))
|
||||
if len(matches) != 14 || strings.Contains(s, "mysecret") {
|
||||
t.Fatal("config's String method reveals authentication credentials.")
|
||||
}
|
||||
|
@ -85,7 +86,7 @@ func TestHideConfigSecrets(t *testing.T) {
|
|||
func TestJSONMarshal(t *testing.T) {
|
||||
c, _, err := LoadFile("testdata/conf.good.yml")
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %s: %s", "testdata/good.yml", err)
|
||||
t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err)
|
||||
}
|
||||
|
||||
_, err = json.Marshal(c)
|
||||
|
@ -114,7 +115,7 @@ func TestJSONMarshalSecret(t *testing.T) {
|
|||
func TestJSONUnmarshalMarshaled(t *testing.T) {
|
||||
c, _, err := LoadFile("testdata/conf.good.yml")
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %s: %s", "testdata/good.yml", err)
|
||||
t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err)
|
||||
}
|
||||
|
||||
plainCfg, err := json.Marshal(c)
|
||||
|
@ -128,3 +129,79 @@ func TestJSONUnmarshalMarshaled(t *testing.T) {
|
|||
t.Fatal("JSON Unmarshaling failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFieldsAndRegex(t *testing.T) {
|
||||
|
||||
boolFoo := true
|
||||
var regexpFoo Regexp
|
||||
regexpFoo.Regexp, _ = regexp.Compile("^(?:^(foo1|foo2|baz)$)$")
|
||||
|
||||
var expectedConf = Config{
|
||||
|
||||
Global: &GlobalConfig{
|
||||
ResolveTimeout: model.Duration(5 * time.Minute),
|
||||
SMTPSmarthost: "localhost:25",
|
||||
SMTPFrom: "alertmanager@example.org",
|
||||
HipchatAuthToken: "mysecret",
|
||||
HipchatURL: "https://hipchat.foobar.org/",
|
||||
SlackAPIURL: "mysecret",
|
||||
SMTPRequireTLS: true,
|
||||
PagerdutyURL: "https://events.pagerduty.com/generic/2010-04-15/create_event.json",
|
||||
OpsGenieAPIHost: "https://api.opsgenie.com/",
|
||||
VictorOpsAPIURL: "https://alert.victorops.com/integrations/generic/20131114/alert/",
|
||||
},
|
||||
|
||||
Templates: []string{
|
||||
"/etc/alertmanager/template/*.tmpl",
|
||||
},
|
||||
Route: &Route{
|
||||
Receiver: "team-X-mails",
|
||||
GroupBy: []model.LabelName{
|
||||
"alertname",
|
||||
"cluster",
|
||||
"service",
|
||||
},
|
||||
Routes: []*Route{
|
||||
{
|
||||
Receiver: "team-X-mails",
|
||||
MatchRE: map[string]Regexp{
|
||||
"service": regexpFoo,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []*Receiver{
|
||||
{
|
||||
Name: "team-X-mails",
|
||||
EmailConfigs: []*EmailConfig{
|
||||
{
|
||||
To: "team-X+alerts@example.org",
|
||||
From: "alertmanager@example.org",
|
||||
Smarthost: "localhost:25",
|
||||
HTML: "{{ template \"email.default.html\" . }}",
|
||||
RequireTLS: &boolFoo,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config, _, err := LoadFile("testdata/conf.empty-fields.yml")
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %s: %s", "testdata/conf.empty-fields.yml", err)
|
||||
}
|
||||
|
||||
configGot, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatal("YAML Marshaling failed:", err)
|
||||
}
|
||||
|
||||
configExp, err := yaml.Marshal(expectedConf)
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(configGot, configExp) {
|
||||
t.Fatalf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.empty-fields.yml", configGot, configExp)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,15 +131,15 @@ type EmailConfig struct {
|
|||
NotifierConfig `yaml:",inline" json:",inline"`
|
||||
|
||||
// Email address to notify.
|
||||
To string `yaml:"to" json:"to"`
|
||||
From string `yaml:"from" json:"from"`
|
||||
To string `yaml:"to,omitempty" json:"to,omitempty"`
|
||||
From string `yaml:"from,omitempty" json:"from,omitempty"`
|
||||
Smarthost string `yaml:"smarthost,omitempty" json:"smarthost,omitempty"`
|
||||
AuthUsername string `yaml:"auth_username" json:"auth_username"`
|
||||
AuthPassword Secret `yaml:"auth_password" json:"auth_password"`
|
||||
AuthSecret Secret `yaml:"auth_secret" json:"auth_secret"`
|
||||
AuthIdentity string `yaml:"auth_identity" json:"auth_identity"`
|
||||
Headers map[string]string `yaml:"headers" json:"headers"`
|
||||
HTML string `yaml:"html" json:"html"`
|
||||
AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"`
|
||||
AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"`
|
||||
AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"`
|
||||
AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"`
|
||||
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||
HTML string `yaml:"html,omitempty" json:"html,omitempty"`
|
||||
RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
|
@ -174,12 +174,12 @@ func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
type PagerdutyConfig struct {
|
||||
NotifierConfig `yaml:",inline" json:",inline"`
|
||||
|
||||
ServiceKey Secret `yaml:"service_key" json:"service_key"`
|
||||
URL string `yaml:"url" json:"url"`
|
||||
Client string `yaml:"client" json:"client"`
|
||||
ClientURL string `yaml:"client_url" json:"client_url"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Details map[string]string `yaml:"details" json:"details"`
|
||||
ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
|
||||
URL string `yaml:"url,omitempty" json:"url,omitempty"`
|
||||
Client string `yaml:"client,omitempty" json:"client,omitempty"`
|
||||
ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
|
@ -202,20 +202,20 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
|
|||
type SlackConfig struct {
|
||||
NotifierConfig `yaml:",inline" json:",inline"`
|
||||
|
||||
APIURL Secret `yaml:"api_url" json:"api_url"`
|
||||
APIURL Secret `yaml:"api_url,omitempty" json:"api_url,omitempty"`
|
||||
|
||||
// Slack channel override, (like #other-channel or @username).
|
||||
Channel string `yaml:"channel" json:"channel"`
|
||||
Username string `yaml:"username" json:"username"`
|
||||
Color string `yaml:"color" json:"color"`
|
||||
Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
|
||||
Username string `yaml:"username,omitempty" json:"username,omitempty"`
|
||||
Color string `yaml:"color,omitempty" json:"color,omitempty"`
|
||||
|
||||
Title string `yaml:"title" json:"title"`
|
||||
TitleLink string `yaml:"title_link" json:"title_link"`
|
||||
Pretext string `yaml:"pretext" json:"pretext"`
|
||||
Text string `yaml:"text" json:"text"`
|
||||
Fallback string `yaml:"fallback" json:"fallback"`
|
||||
IconEmoji string `yaml:"icon_emoji" json:"icon_emoji"`
|
||||
IconURL string `yaml:"icon_url" json:"icon_url"`
|
||||
Title string `yaml:"title,omitempty" json:"title,omitempty"`
|
||||
TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"`
|
||||
Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"`
|
||||
Text string `yaml:"text,omitempty" json:"text,omitempty"`
|
||||
Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"`
|
||||
IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"`
|
||||
IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
|
@ -235,17 +235,17 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
type HipchatConfig struct {
|
||||
NotifierConfig `yaml:",inline" json:",inline"`
|
||||
|
||||
APIURL string `yaml:"api_url" json:"api_url"`
|
||||
AuthToken Secret `yaml:"auth_token" json:"auth_token"`
|
||||
RoomID string `yaml:"room_id" json:"room_id"`
|
||||
From string `yaml:"from" json:"from"`
|
||||
Notify bool `yaml:"notify" json:"notify"`
|
||||
Message string `yaml:"message" json:"message"`
|
||||
MessageFormat string `yaml:"message_format" json:"message_format"`
|
||||
Color string `yaml:"color" json:"color"`
|
||||
APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"`
|
||||
AuthToken Secret `yaml:"auth_token,omitempty" json:"auth_token,omitempty"`
|
||||
RoomID string `yaml:"room_id,omitempty" json:"room_id,omitempty"`
|
||||
From string `yaml:"from,omitempty" json:"from,omitempty"`
|
||||
Notify bool `yaml:"notify,omitempty" json:"notify,omitempty"`
|
||||
Message string `yaml:"message,omitempty" json:"message,omitempty"`
|
||||
MessageFormat string `yaml:"message_format,omitempty" json:"message_format,omitempty"`
|
||||
Color string `yaml:"color,omitempty" json:"color,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
XXX map[string]interface{} `yaml:",inline" ,json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
|
@ -290,15 +290,15 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
type OpsGenieConfig struct {
|
||||
NotifierConfig `yaml:",inline" json:",inline"`
|
||||
|
||||
APIKey Secret `yaml:"api_key" json:"api_key"`
|
||||
APIHost string `yaml:"api_host" json:"api_host"`
|
||||
Message string `yaml:"message" json:"message"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Source string `yaml:"source" json:"source"`
|
||||
Details map[string]string `yaml:"details" json:"details"`
|
||||
Teams string `yaml:"teams" json:"teams"`
|
||||
Tags string `yaml:"tags" json:"tags"`
|
||||
Note string `yaml:"note" json:"note"`
|
||||
APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"`
|
||||
APIHost string `yaml:"api_host,omitempty" json:"api_host,omitempty"`
|
||||
Message string `yaml:"message,omitempty" json:"message,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
|
||||
Teams string `yaml:"teams,omitempty" json:"teams,omitempty"`
|
||||
Tags string `yaml:"tags,omitempty" json:"tags,omitempty"`
|
||||
Note string `yaml:"note,omitempty" json:"note,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
|
@ -321,12 +321,12 @@ func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
|
|||
type VictorOpsConfig struct {
|
||||
NotifierConfig `yaml:",inline" json:",inline"`
|
||||
|
||||
APIKey Secret `yaml:"api_key" json:"api_key"`
|
||||
APIURL string `yaml:"api_url" json:"api_url"`
|
||||
RoutingKey string `yaml:"routing_key" json:"routing_key"`
|
||||
MessageType string `yaml:"message_type" json:"message_type"`
|
||||
StateMessage string `yaml:"state_message" json:"state_message"`
|
||||
MonitoringTool string `yaml:"monitoring_tool" json:"monitoring_tool"`
|
||||
APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"`
|
||||
APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"`
|
||||
RoutingKey string `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
|
||||
MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"`
|
||||
StateMessage string `yaml:"state_message,omitempty" json:"state_message,omitempty"`
|
||||
MonitoringTool string `yaml:"monitoring_tool,omitempty" json:"monitoring_tool,omitempty"`
|
||||
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
}
|
||||
|
@ -364,14 +364,14 @@ func (d duration) MarshalText() ([]byte, error) {
|
|||
type PushoverConfig struct {
|
||||
NotifierConfig `yaml:",inline" json:",inline"`
|
||||
|
||||
UserKey Secret `yaml:"user_key" json:"user_key"`
|
||||
Token Secret `yaml:"token" json:"token"`
|
||||
Title string `yaml:"title" json:"title"`
|
||||
Message string `yaml:"message" json:"message"`
|
||||
URL string `yaml:"url" json:"url"`
|
||||
Priority string `yaml:"priority" json:"priority"`
|
||||
Retry duration `yaml:"retry" json:"retry"`
|
||||
Expire duration `yaml:"expire" json:"expire"`
|
||||
UserKey Secret `yaml:"user_key,omitempty" json:"user_key,omitempty"`
|
||||
Token Secret `yaml:"token,omitempty" json:"token,omitempty"`
|
||||
Title string `yaml:"title,omitempty" json:"title,omitempty"`
|
||||
Message string `yaml:"message,omitempty" json:"message,omitempty"`
|
||||
URL string `yaml:"url,omitempty" json:"url,omitempty"`
|
||||
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
|
||||
Retry duration `yaml:"retry,omitempty" json:"retry,omitempty"`
|
||||
Expire duration `yaml:"expire,omitempty" json:"expire,omitempty"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline" json:"-"`
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
global:
|
||||
smtp_smarthost: 'localhost:25'
|
||||
smtp_from: 'alertmanager@example.org'
|
||||
smtp_auth_username: ''
|
||||
smtp_auth_password: ''
|
||||
hipchat_auth_token: 'mysecret'
|
||||
hipchat_url: 'https://hipchat.foobar.org/'
|
||||
slack_api_url: 'mysecret'
|
||||
|
||||
|
||||
|
||||
templates:
|
||||
- '/etc/alertmanager/template/*.tmpl'
|
||||
|
||||
route:
|
||||
group_by: ['alertname', 'cluster', 'service']
|
||||
|
||||
receiver: team-X-mails
|
||||
routes:
|
||||
- match_re:
|
||||
service: ^(foo1|foo2|baz)$
|
||||
receiver: team-X-mails
|
||||
|
||||
receivers:
|
||||
- name: 'team-X-mails'
|
||||
email_configs:
|
||||
- to: 'team-X+alerts@example.org'
|
Loading…
Reference in New Issue