Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config equality logic + simple unit tests #862

Merged
merged 2 commits into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/alertmanager/alertmanager_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func startOpniServer(configFile string) {
http.Error(wr, err.Error(), http.StatusInternalServerError)
return
}
if equal, reason := r1.IsEqual(r2); !equal {
if equal, reason := r1.Equal(r2); !equal {
lg.Errorf("config is not equal to persisted config: %s", reason)
http.Error(wr, fmt.Sprintf("config not yet equal : %s", reason), http.StatusConflict)
return
Expand Down
2 changes: 1 addition & 1 deletion pkg/alerting/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ func NewExpectConfigEqual(expectedConfig string) func(*http.Response) error {
if err != nil {
return err
}
if isEqual, reason := r1.IsEqual(r2); !isEqual {
if isEqual, reason := r1.Equal(r2); !isEqual {
lg.Debug(fmt.Sprintf("config not equal : %s", reason))
return fmt.Errorf("%s", reason)
}
Expand Down
181 changes: 6 additions & 175 deletions pkg/alerting/routing/api_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,17 @@ import (
"net/url"
"time"

"github.com/containerd/containerd/pkg/cri/config"
cfg "github.com/prometheus/alertmanager/config"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

var _ OpniReceiver = (*Receiver)(nil)

var (
DefaultSlackConfig = SlackConfig{
NotifierConfig: cfg.NotifierConfig{
VSendResolved: false,
},
Color: `{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}`,
Username: `{{ template "slack.default.username" . }}`,
Title: `{{ template "slack.default.title" . }}`,
TitleLink: `{{ template "slack.default.titlelink" . }}`,
IconEmoji: `{{ template "slack.default.iconemoji" . }}`,
IconURL: `{{ template "slack.default.iconurl" . }}`,
Pretext: `{{ template "slack.default.pretext" . }}`,
Text: `{{ template "slack.default.text" . }}`,
Fallback: `{{ template "slack.default.fallback" . }}`,
CallbackID: `{{ template "slack.default.callbackid" . }}`,
Footer: `{{ template "slack.default.footer" . }}`,
}
// DefaultEmailConfig defines default values for Email configurations.
DefaultEmailConfig = EmailConfig{
NotifierConfig: cfg.NotifierConfig{
VSendResolved: false,
},
HTML: `{{ template "email.default.html" . }}`,
Text: ``,
}
normalizeTitle = cases.Title(language.AmericanEnglish)

// DefaultPagerdutyConfig defines default values for PagerDuty configurations.
DefaultPagerdutyConfig = PagerdutyConfig{
NotifierConfig: &cfg.NotifierConfig{
VSendResolved: true,
},
Description: `{{ template "pagerduty.default.description" .}}`,
Client: `{{ template "pagerduty.default.client" . }}`,
ClientURL: `{{ template "pagerduty.default.clientURL" . }}`,
}
// DefaultPagerdutyDetails defines the default values for PagerDuty details.
DefaultPagerdutyDetails = map[string]string{
"firing": `{{ template "pagerduty.default.instances" .Alerts.Firing }}`,
"resolved": `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
"num_firing": `{{ .Alerts.Firing | len }}`,
"num_resolved": `{{ .Alerts.Resolved | len }}`,
}
)

// Receiver configuration provides configuration on how to contact a receiver.
Expand All @@ -76,6 +36,10 @@ type Receiver struct {
TelegramConfigs []*cfg.TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"`
}

func (r *Receiver) Equal(other *Receiver) (bool, string) {
return receiversAreEqual(r, other)
}

func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain Receiver
if err := unmarshal((*plain)(c)); err != nil {
Expand All @@ -87,139 +51,6 @@ func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}

type SlackConfig struct {
cfg.NotifierConfig `yaml:",inline" json:",inline"`

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

// string since the string is stored in a kube secret anyways
APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"`
APIURLFile string `yaml:"api_url_file,omitempty" json:"api_url_file,omitempty"`

// Slack channel override, (like #other-channel or @username).
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,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"`
Fields []*cfg.SlackField `yaml:"fields,omitempty" json:"fields,omitempty"`
ShortFields bool `yaml:"short_fields" json:"short_fields,omitempty"`
Footer string `yaml:"footer,omitempty" json:"footer,omitempty"`
Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"`
CallbackID string `yaml:"callback_id,omitempty" json:"callback_id,omitempty"`
IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"`
IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"`
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
ThumbURL string `yaml:"thumb_url,omitempty" json:"thumb_url,omitempty"`
LinkNames bool `yaml:"link_names" json:"link_names,omitempty"`
MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"`
Actions []*cfg.SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"`
}

type EmailConfig struct {
cfg.NotifierConfig `yaml:",inline" json:",inline"`
To string `yaml:"to,omitempty" json:"to,omitempty"`
From string `yaml:"from,omitempty" json:"from,omitempty"`
Hello string `yaml:"hello,omitempty" json:"hello,omitempty"`
Smarthost cfg.HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"`
AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"`
// Change from secret to string since the string is stored in a kube secret anyways
AuthPassword string `yaml:"auth_password,omitempty" json:"auth_password,omitempty"`
// Change from secret to string since the string is stored in a kube secret anyways
AuthSecret string `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"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"`
TLSConfig config.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultEmailConfig
type plain EmailConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.To == "" {
return fmt.Errorf("missing to address in email config")
}
// Header names are case-insensitive, check for collisions.
normalizedHeaders := map[string]string{}
for h, v := range c.Headers {
normalized := normalizeTitle.String(h)
if _, ok := normalizedHeaders[normalized]; ok {
return fmt.Errorf("duplicate header %q in email config", normalized)
}
normalizedHeaders[normalized] = v
}
c.Headers = normalizedHeaders

return nil
}

func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultSlackConfig
type plain SlackConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}

if c.APIURL != "" && len(c.APIURLFile) > 0 {
return fmt.Errorf("at most one of api_url & api_url_file must be configured")
}

return nil
}

// PagerdutyConfig configures notifications via PagerDuty.
type PagerdutyConfig struct {
*cfg.NotifierConfig `yaml:",inline" json:",inline"`

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

// Change from secret to string since the string is stored in a kube secret anyways
ServiceKey string `yaml:"service_key,omitempty" json:"service_key,omitempty"`
// Change from secret to string since the string is stored in a kube secret anyways
RoutingKey string `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
URL *cfg.URL `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"`
Images []*cfg.PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
Links []*cfg.PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`
Class string `yaml:"class,omitempty" json:"class,omitempty"`
Component string `yaml:"component,omitempty" json:"component,omitempty"`
Group string `yaml:"group,omitempty" json:"group,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultPagerdutyConfig
type plain PagerdutyConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.RoutingKey == "" && c.ServiceKey == "" {
return fmt.Errorf("missing service or routing key in PagerDuty config")
}
if c.Details == nil {
c.Details = make(map[string]string)
}
for k, v := range DefaultPagerdutyDetails {
if _, ok := c.Details[k]; !ok {
c.Details[k] = v
}
}
return nil
}

// required due to https://github.com/rancher/opni/issues/542
type GlobalConfig struct {
// ResolveTimeout is the time after which an alert is declared resolved
Expand Down
70 changes: 22 additions & 48 deletions pkg/alerting/routing/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,67 +68,35 @@ func areDurationsEqual(m1, m2 *model.Duration) (equal bool, reason string) {

}

func slackConfigsAreEqual(s1, s2 *SlackConfig) (equal bool, reason string) {
if s1.Channel != s2.Channel {
return false, fmt.Sprintf("channel mismatch %s <-> %s ", s1.Channel, s2.Channel)
}
if s1.APIURL != s2.APIURL {
return false, fmt.Sprintf("api url mismatch %s <-> %s ", s1.APIURL, s2.APIURL)
}
return true, ""
}

func emailConfigsAreEqual(e1, e2 *EmailConfig) (equal bool, reason string) {
if e1.To == e2.To {
return false, fmt.Sprintf("to mismatch %s <-> %s", e1.To, e2.To)
}
if e1.From == e2.From {
return false, fmt.Sprintf("from mismatch %s <-> %s ", e1.From, e2.From)
}
if e1.Smarthost == e2.Smarthost {
return false, fmt.Sprintf("smarthost mismatch %s <-> %s ", e1.Smarthost, e2.Smarthost)
}
if e1.AuthUsername == e2.AuthUsername {
return false, fmt.Sprintf("auth username mismatch %s <-> %s ", e1.AuthUsername, e2.AuthUsername)
}
if e1.AuthPassword == e2.AuthPassword {
return false, fmt.Sprintf("auth password mismatch %s <-> %s ", e1.AuthPassword, e2.AuthPassword)
}
if e1.AuthSecret == e2.AuthSecret {
return false, fmt.Sprintf("auth secret mismatch %s <-> %s ", e1.AuthSecret, e2.AuthSecret)
}
if e1.RequireTLS == e2.RequireTLS {
return false, fmt.Sprintf("require tls mismatch %v <-> %v ", e1.RequireTLS, e2.RequireTLS)
}
if e1.HTML == e2.HTML {
return false, fmt.Sprintf("html mismatch %s <-> %s ", e1.HTML, e2.HTML)
}
if e1.Text == e2.Text {
return false, fmt.Sprintf("text mismatch %s <-> %s ", e1.Text, e2.Text)
}
return true, ""
}

func receiversAreEqual(r1 *Receiver, r2 *Receiver) (equal bool, reason string) {
if r1.Name != r2.Name {
if r1.Name != r2.Name { // opni specific indexing
return false, fmt.Sprintf("receiver name mismatch %s <-> %s ", r1.Name, r2.Name)
}
if len(r1.EmailConfigs) != len(r2.EmailConfigs) {
return false, fmt.Sprintf("email config length mismatch %d <-> %d ", len(r1.EmailConfigs), len(r2.EmailConfigs))
return false, fmt.Sprintf("email configs are not yet synced: found num old %d <-> num new %d ", len(r1.EmailConfigs), len(r2.EmailConfigs))
}

if len(r1.SlackConfigs) != len(r2.SlackConfigs) {
return false, "slack config length mismatch"
return false, fmt.Sprintf("slack configs are not yet synced: found num old %d <-> num new %d ", len(r1.SlackConfigs), len(r2.SlackConfigs))
}
if len(r1.PagerdutyConfigs) != len(r2.PagerdutyConfigs) {
return false, fmt.Sprintf("pager duty configs are not yet synced: found num old %d <-> num new %d ", len(r1.PagerdutyConfigs), len(r2.PagerdutyConfigs))
}
for idx, emailConfig := range r1.EmailConfigs {
if equal, reason := emailConfigsAreEqual(emailConfig, r2.EmailConfigs[idx]); !equal {
if equal, reason := emailConfig.Equal(r2.EmailConfigs[idx]); !equal {
return false, fmt.Sprintf("email config mismatch : %s", reason)
}
}
for idx, slackConfig := range r1.SlackConfigs {
if equal, reason := slackConfigsAreEqual(slackConfig, r2.SlackConfigs[idx]); !equal {
if equal, reason := slackConfig.Equal(r2.SlackConfigs[idx]); !equal {
return false, fmt.Sprintf("slack config mismatch %s", reason)
}
}
for idx, pagerDutyConfig := range r1.PagerdutyConfigs {
if equal, reason := pagerDutyConfig.Equal(r2.PagerdutyConfigs[idx]); !equal {
return false, fmt.Sprintf("pager duty config mismatch %s", reason)
}
}
return true, ""
}

Expand Down Expand Up @@ -205,15 +173,21 @@ func areMatchersEqual(m1, m2 cfg.Matchers) bool {
return true
}

var _ EqualityComparer[any] = (*RoutingTree)(nil)

// for our purposes we will only treat receivers and routes as opni config equality
func (r *RoutingTree) IsEqual(other *RoutingTree) (equal bool, reason string) {
func (r *RoutingTree) Equal(input any) (equal bool, reason string) {
if _, ok := input.(*RoutingTree); !ok {
return false, "input is not a routing tree"
}
other := input.(*RoutingTree)
selfReceiverIndex := r.indexOpniReceivers()
otherReceiverIndex := other.indexOpniReceivers()
for id, r1 := range selfReceiverIndex {
if r2, ok := otherReceiverIndex[id]; !ok {
return false, fmt.Sprintf("configurations do not have matching receiver : %s", id)
} else {
if equal, reason := receiversAreEqual(r1, r2); !equal {
if equal, reason := r1.Equal(r2); !equal {
return false, fmt.Sprintf("configurations do not have equal receivers '%s' : %s", id, reason)
}
}
Expand Down
Loading