Skip to content

Commit

Permalink
Equality interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandreLamarre committed Dec 14, 2022
1 parent a161863 commit 03d7a92
Show file tree
Hide file tree
Showing 13 changed files with 448 additions and 295 deletions.
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
79 changes: 11 additions & 68 deletions pkg/alerting/routing/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,69 +68,6 @@ 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 pagerDutyConfigsAreEqual(p1, p2 *PagerdutyConfig) (equal bool, reason string) {
if p1.RoutingKey != p2.RoutingKey {
return false, fmt.Sprintf("routing key mismatch %s <-> %s ", p1.RoutingKey, p2.RoutingKey)
}
if p1.ServiceKey != p2.ServiceKey {
return false, fmt.Sprintf("service key mismatch %s <-> %s ", p1.ServiceKey, p2.ServiceKey)
}
if p1.URL != p2.URL {
return false, fmt.Sprintf("url mismatch %s <-> %s ", p1.URL, p2.URL)
}
if p1.Client != p2.Client {
return false, fmt.Sprintf("client mismatch %s <-> %s ", p1.Client, p2.Client)
}
if p1.ClientURL != p2.ClientURL {
return false, fmt.Sprintf("client url mismatch %s <-> %s ", p1.ClientURL, p2.ClientURL)
}
if p1.Description != p2.Description {
return false, fmt.Sprintf("description mismatch %s <-> %s ", p1.Description, p2.Description)
}
return true, ""
}

func receiversAreEqual(r1 *Receiver, r2 *Receiver) (equal bool, reason string) {
if r1.Name != r2.Name { // opni specific indexing
return false, fmt.Sprintf("receiver name mismatch %s <-> %s ", r1.Name, r2.Name)
Expand All @@ -146,17 +83,17 @@ func receiversAreEqual(r1 *Receiver, r2 *Receiver) (equal bool, reason string) {
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 := pagerDutyConfigsAreEqual(pagerDutyConfig, r2.PagerdutyConfigs[idx]); !equal {
if equal, reason := pagerDutyConfig.Equal(r2.PagerdutyConfigs[idx]); !equal {
return false, fmt.Sprintf("pager duty config mismatch %s", reason)
}
}
Expand Down Expand Up @@ -236,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

0 comments on commit 03d7a92

Please sign in to comment.