From c70b82e9ab5ed1dddaa20e2265a8ac37c84f01ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorben=20G=C3=BCnther?= Date: Wed, 12 Jul 2023 14:56:48 +0200 Subject: [PATCH] config: Move to own package --- config.go => config/config.go | 117 +++++++++++++----------- config_test.go => config/config_test.go | 24 ++--- main.go | 67 +++++++------- silence.go | 10 +- 4 files changed, 113 insertions(+), 105 deletions(-) rename config.go => config/config.go (71%) rename config_test.go => config/config_test.go (85%) diff --git a/config.go b/config/config.go similarity index 71% rename from config.go rename to config/config.go index 6bf6f24..74bd8ff 100644 --- a/config.go +++ b/config/config.go @@ -1,4 +1,5 @@ -package main +// Package config defines the configuration file. +package config import ( "errors" @@ -9,32 +10,37 @@ import ( "git.sr.ht/~emersion/go-scfg" ) -type alertMode int +// AlertMode determines if alerts grouped by Alertmanager are being kept together. +type AlertMode int +// The different modes for AlertMode. const ( - single alertMode = iota - multi + Single AlertMode = iota + Multi ) -type cacheType int +// CacheType is the type of cache that well be used. +type CacheType int +// The different types of caches. const ( - memory cacheType = iota - redis + Memory CacheType = iota + Redis ) -type config struct { +// Config is the configuration of the bridge. +type Config struct { BaseURL string HTTPAddress string LogLevel string - alertMode alertMode + AlertMode AlertMode User string Password string - ntfy ntfyConfig - labels labels - cache cacheConfig - am alertmanagerConfig - resolved resolvedConfig + Ntfy ntfyConfig + Labels labels + Cache cacheConfig + Am alertmanagerConfig + Resolved resolvedConfig } type ntfyConfig struct { @@ -42,8 +48,8 @@ type ntfyConfig struct { User string Password string AccessToken string - emailAddress string - call string + EmailAddress string + Call string } type labels struct { @@ -55,13 +61,13 @@ type labelConfig struct { Priority string Tags []string Icon string - emailAddress string - call string + EmailAddress string + Call string } type cacheConfig struct { // shared settings - Type cacheType + Type CacheType Duration time.Duration // memory settings CleanupInterval time.Duration @@ -81,24 +87,25 @@ type resolvedConfig struct { Icon string } -func readConfig(path string) (*config, error) { +// ReadConfig reads an scfg formatted file and returns the configuration struct. +func ReadConfig(path string) (*Config, error) { cfg, err := scfg.Load(path) if err != nil { return nil, err } - config := new(config) + config := new(Config) // Set default values config.HTTPAddress = "127.0.0.1:8080" config.LogLevel = "info" - config.alertMode = single + config.AlertMode = Single - config.cache.Type = memory - config.cache.Duration = time.Hour * 24 + config.Cache.Type = Memory + config.Cache.Duration = time.Hour * 24 // memory - config.cache.CleanupInterval = time.Hour + config.Cache.CleanupInterval = time.Hour // redis - config.cache.RedisURL = "redis://localhost:6379" + config.Cache.RedisURL = "redis://localhost:6379" d := cfg.Get("log-level") if d != nil { @@ -130,10 +137,10 @@ func readConfig(path string) (*config, error) { switch strings.ToLower(mode) { case "single": - config.alertMode = single + config.AlertMode = Single case "multi": - config.alertMode = multi + config.AlertMode = Multi default: return nil, fmt.Errorf("%q directive: illegal mode %q", d.Name, mode) @@ -168,11 +175,11 @@ func readConfig(path string) (*config, error) { return nil, err } - config.labels.Order = strings.Split(order, ",") + config.Labels.Order = strings.Split(order, ",") } labels := make(map[string]labelConfig) - for _, labelName := range config.labels.Order { + for _, labelName := range config.Labels.Order { for _, labelDir := range labelsDir.Children.GetAll(labelName) { labelConfig := new(labelConfig) var name string @@ -207,14 +214,14 @@ func readConfig(path string) (*config, error) { d = labelDir.Children.Get("email-address") if d != nil { - if err := d.ParseParams(&labelConfig.emailAddress); err != nil { + if err := d.ParseParams(&labelConfig.EmailAddress); err != nil { return nil, err } } d = labelDir.Children.Get("call") if d != nil { - if err := d.ParseParams(&labelConfig.call); err != nil { + if err := d.ParseParams(&labelConfig.Call); err != nil { return nil, err } } @@ -223,7 +230,7 @@ func readConfig(path string) (*config, error) { } } - config.labels.Label = labels + config.Labels.Label = labels } ntfyDir := cfg.Get("ntfy") @@ -235,50 +242,50 @@ func readConfig(path string) (*config, error) { if d == nil { return nil, fmt.Errorf("%q missing from %q directive", "topic", "ntfy") } - if err := d.ParseParams(&config.ntfy.Topic); err != nil { + if err := d.ParseParams(&config.Ntfy.Topic); err != nil { return nil, err } d = ntfyDir.Children.Get("user") if d != nil { - if err := d.ParseParams(&config.ntfy.User); err != nil { + if err := d.ParseParams(&config.Ntfy.User); err != nil { return nil, err } } d = ntfyDir.Children.Get("password") if d != nil { - if err := d.ParseParams(&config.ntfy.Password); err != nil { + if err := d.ParseParams(&config.Ntfy.Password); err != nil { return nil, err } } - if (config.ntfy.Password != "" && config.ntfy.User == "") || - (config.ntfy.Password == "" && config.ntfy.User != "") { + if (config.Ntfy.Password != "" && config.Ntfy.User == "") || + (config.Ntfy.Password == "" && config.Ntfy.User != "") { return nil, errors.New("ntfy: user and password have to be set together") } d = ntfyDir.Children.Get("access-token") if d != nil { - if err := d.ParseParams(&config.ntfy.AccessToken); err != nil { + if err := d.ParseParams(&config.Ntfy.AccessToken); err != nil { return nil, err } } - if config.ntfy.User != "" && config.ntfy.AccessToken != "" { + if config.Ntfy.User != "" && config.Ntfy.AccessToken != "" { return nil, errors.New("ntfy: cannot use both an access-token and a user/password at the same time") } d = ntfyDir.Children.Get("email-address") if d != nil { - if err := d.ParseParams(&config.ntfy.emailAddress); err != nil { + if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil { return nil, err } } d = ntfyDir.Children.Get("call") if d != nil { - if err := d.ParseParams(&config.ntfy.call); err != nil { + if err := d.ParseParams(&config.Ntfy.Call); err != nil { return nil, err } } @@ -295,9 +302,9 @@ func readConfig(path string) (*config, error) { switch strings.ToLower(cacheType) { case "memory": - config.cache.Type = memory + config.Cache.Type = Memory case "redis": - config.cache.Type = redis + config.Cache.Type = Redis default: return nil, fmt.Errorf("cache: illegal type %q", cacheType) } @@ -315,7 +322,7 @@ func readConfig(path string) (*config, error) { return nil, err } - config.cache.Duration = duration + config.Cache.Duration = duration } // memory @@ -331,13 +338,13 @@ func readConfig(path string) (*config, error) { return nil, err } - config.cache.CleanupInterval = interval + config.Cache.CleanupInterval = interval } // redis d = cacheDir.Children.Get("redis-url") if d != nil { - if err := d.ParseParams(&config.cache.RedisURL); err != nil { + if err := d.ParseParams(&config.Cache.RedisURL); err != nil { return nil, err } } @@ -358,31 +365,31 @@ func readConfig(path string) (*config, error) { return nil, err } - config.am.SilenceDuration = duration + config.Am.SilenceDuration = duration } d = amDir.Children.Get("user") if d != nil { - if err := d.ParseParams(&config.am.User); err != nil { + if err := d.ParseParams(&config.Am.User); err != nil { return nil, err } } d = amDir.Children.Get("password") if d != nil { - if err := d.ParseParams(&config.am.Password); err != nil { + if err := d.ParseParams(&config.Am.Password); err != nil { return nil, err } } - if (config.am.Password != "" && config.am.User == "") || - (config.am.Password == "" && config.am.User != "") { + if (config.Am.Password != "" && config.Am.User == "") || + (config.Am.Password == "" && config.Am.User != "") { return nil, errors.New("alertmanager: user and password have to be set together") } d = amDir.Children.Get("url") if d != nil { - if err := d.ParseParams(&config.am.URL); err != nil { + if err := d.ParseParams(&config.Am.URL); err != nil { return nil, err } } @@ -397,12 +404,12 @@ func readConfig(path string) (*config, error) { return nil, err } - config.resolved.Tags = strings.Split(tags, ",") + config.Resolved.Tags = strings.Split(tags, ",") } d = resolvedDir.Children.Get("icon") if d != nil { - if err := d.ParseParams(&config.resolved.Icon); err != nil { + if err := d.ParseParams(&config.Resolved.Icon); err != nil { return nil, err } } diff --git a/config_test.go b/config/config_test.go similarity index 85% rename from config_test.go rename to config/config_test.go index 774479c..98bd562 100644 --- a/config_test.go +++ b/config/config_test.go @@ -1,4 +1,4 @@ -package main +package config import ( "os" @@ -62,40 +62,40 @@ cache { } ` - expectedCfg := &config{ + expectedCfg := &Config{ BaseURL: "https://ntfy-alertmanager.xenrox.net", HTTPAddress: ":8080", LogLevel: "info", - alertMode: multi, + AlertMode: Multi, User: "webhookUser", Password: "webhookPass", - ntfy: ntfyConfig{Topic: "https://ntfy.sh/alertmanager-alerts", User: "user", Password: "pass"}, - labels: labels{Order: []string{"severity", "instance"}, + Ntfy: ntfyConfig{Topic: "https://ntfy.sh/alertmanager-alerts", User: "user", Password: "pass"}, + Labels: labels{Order: []string{"severity", "instance"}, Label: map[string]labelConfig{ "severity:critical": { Priority: "5", Tags: []string{"rotating_light"}, Icon: "https://foo.com/critical.png", - emailAddress: "foo@bar.com", - call: "yes", + EmailAddress: "foo@bar.com", + Call: "yes", }, "severity:info": {Priority: "1"}, "instance:example.com": {Tags: []string{"computer", "example"}}, }, }, - cache: cacheConfig{ - Type: redis, + Cache: cacheConfig{ + Type: Redis, Duration: 48 * time.Hour, CleanupInterval: time.Hour, RedisURL: "redis://user:password@localhost:6789/3", }, - am: alertmanagerConfig{ + Am: alertmanagerConfig{ SilenceDuration: time.Hour * 24, User: "user", Password: "pass", URL: "https://alertmanager.xenrox.net", }, - resolved: resolvedConfig{ + Resolved: resolvedConfig{ Tags: []string{"resolved", "partying_face"}, Icon: "https://foo.com/resolved.png", }, @@ -107,7 +107,7 @@ cache { t.Errorf("failed to write config file: %v", err) } - cfg, err := readConfig(configPath) + cfg, err := ReadConfig(configPath) if err != nil { t.Errorf("failed to read config file: %v", err) } diff --git a/main.go b/main.go index 8f97b64..cf15fec 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "git.xenrox.net/~xenrox/go-log" "git.xenrox.net/~xenrox/ntfy-alertmanager/cache" + "git.xenrox.net/~xenrox/ntfy-alertmanager/config" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -23,7 +24,7 @@ import ( var version = "dev" type bridge struct { - cfg *config + cfg *config.Config logger *log.Logger cache cache.Cache client *httpClient @@ -102,20 +103,20 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification { var tags []string if alert.Status == "resolved" { - tags = append(tags, br.cfg.resolved.Tags...) - n.icon = br.cfg.resolved.Icon + tags = append(tags, br.cfg.Resolved.Tags...) + n.icon = br.cfg.Resolved.Icon } - n.emailAddress = br.cfg.ntfy.emailAddress - n.call = br.cfg.ntfy.call + n.emailAddress = br.cfg.Ntfy.EmailAddress + n.call = br.cfg.Ntfy.Call - for _, labelName := range br.cfg.labels.Order { + for _, labelName := range br.cfg.Labels.Order { val, ok := alert.Labels[labelName] if !ok { continue } - labelConfig, ok := br.cfg.labels.Label[fmt.Sprintf("%s:%s", labelName, val)] + labelConfig, ok := br.cfg.Labels.Label[fmt.Sprintf("%s:%s", labelName, val)] if !ok { continue } @@ -129,11 +130,11 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification { } if n.emailAddress == "" { - n.emailAddress = labelConfig.emailAddress + n.emailAddress = labelConfig.EmailAddress } if n.call == "" { - n.call = labelConfig.call + n.call = labelConfig.Call } for _, val := range labelConfig.Tags { @@ -145,7 +146,7 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification { n.tags = strings.Join(tags, ",") - if br.cfg.am.SilenceDuration != 0 && alert.Status == "firing" { + if br.cfg.Am.SilenceDuration != 0 && alert.Status == "firing" { if br.cfg.BaseURL == "" { br.logger.Error("Failed to create silence action: No base-url set") } else { @@ -210,20 +211,20 @@ func (br *bridge) multiAlertNotification(p *payload) *notification { var tags []string if p.Status == "resolved" { - tags = append(tags, br.cfg.resolved.Tags...) - n.icon = br.cfg.resolved.Icon + tags = append(tags, br.cfg.Resolved.Tags...) + n.icon = br.cfg.Resolved.Icon } - n.emailAddress = br.cfg.ntfy.emailAddress - n.call = br.cfg.ntfy.call + n.emailAddress = br.cfg.Ntfy.EmailAddress + n.call = br.cfg.Ntfy.Call - for _, labelName := range br.cfg.labels.Order { + for _, labelName := range br.cfg.Labels.Order { val, ok := p.CommonLabels[labelName] if !ok { continue } - labelConfig, ok := br.cfg.labels.Label[fmt.Sprintf("%s:%s", labelName, val)] + labelConfig, ok := br.cfg.Labels.Label[fmt.Sprintf("%s:%s", labelName, val)] if !ok { continue } @@ -237,11 +238,11 @@ func (br *bridge) multiAlertNotification(p *payload) *notification { } if n.emailAddress == "" { - n.emailAddress = labelConfig.emailAddress + n.emailAddress = labelConfig.EmailAddress } if n.call == "" { - n.call = labelConfig.call + n.call = labelConfig.Call } for _, val := range labelConfig.Tags { @@ -253,7 +254,7 @@ func (br *bridge) multiAlertNotification(p *payload) *notification { n.tags = strings.Join(tags, ",") - if br.cfg.am.SilenceDuration != 0 && p.Status == "firing" { + if br.cfg.Am.SilenceDuration != 0 && p.Status == "firing" { if br.cfg.BaseURL == "" { br.logger.Error("Failed to create silence action: No base-url set") } else { @@ -272,16 +273,16 @@ func (br *bridge) multiAlertNotification(p *payload) *notification { } func (br *bridge) publish(n *notification) error { - req, err := http.NewRequest(http.MethodPost, br.cfg.ntfy.Topic, strings.NewReader(n.body)) + req, err := http.NewRequest(http.MethodPost, br.cfg.Ntfy.Topic, strings.NewReader(n.body)) if err != nil { return err } // ntfy authentication - if br.cfg.ntfy.Password != "" && br.cfg.ntfy.User != "" { - req.SetBasicAuth(br.cfg.ntfy.User, br.cfg.ntfy.Password) - } else if br.cfg.ntfy.AccessToken != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", br.cfg.ntfy.AccessToken)) + if br.cfg.Ntfy.Password != "" && br.cfg.Ntfy.User != "" { + req.SetBasicAuth(br.cfg.Ntfy.User, br.cfg.Ntfy.Password) + } else if br.cfg.Ntfy.AccessToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", br.cfg.Ntfy.AccessToken)) } req.Header.Set("X-Title", n.title) @@ -363,7 +364,7 @@ func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) { br.logger.Debugf("Received alert %+v", event) } - if br.cfg.alertMode == single { + if br.cfg.AlertMode == config.Single { notifications := br.singleAlertNotifications(&event) for _, n := range notifications { err := br.publish(n) @@ -412,7 +413,7 @@ func (br *bridge) basicAuthMiddleware(handler http.HandlerFunc) http.HandlerFunc func (br *bridge) runCleanup() { for { - time.Sleep(br.cfg.cache.CleanupInterval) + time.Sleep(br.cfg.Cache.CleanupInterval) br.logger.Info("Pruning cache") br.cache.Cleanup() } @@ -432,7 +433,7 @@ func main() { logger := log.NewDefaultLogger() - cfg, err := readConfig(configPath) + cfg, err := config.ReadConfig(configPath) if err != nil { logger.Fatalf("Failed to read config: %v", err) } @@ -444,12 +445,12 @@ func main() { client := &httpClient{&http.Client{Timeout: time.Second * 3}} var c cache.Cache - switch cfg.cache.Type { - case memory: - c = cache.NewMemoryCache(cfg.cache.Duration) - case redis: + switch cfg.Cache.Type { + case config.Memory: + c = cache.NewMemoryCache(cfg.Cache.Duration) + case config.Redis: var err error - c, err = cache.NewRedisCache(cfg.cache.RedisURL, cfg.cache.Duration) + c, err = cache.NewRedisCache(cfg.Cache.RedisURL, cfg.Cache.Duration) if err != nil { logger.Fatalf("Failed to create redis cache: %v", err) } @@ -467,7 +468,7 @@ func main() { http.HandleFunc("/silences", bridge.handleSilences) } - if cfg.cache.Type == memory { + if cfg.Cache.Type == config.Memory { go bridge.runCleanup() } logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil)) diff --git a/silence.go b/silence.go index b4cfc32..d372c29 100644 --- a/silence.go +++ b/silence.go @@ -77,7 +77,7 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { silence := &silence{ StartsAt: time.Now().UTC().Format(dateLayout), - EndsAt: time.Now().Add(br.cfg.am.SilenceDuration).UTC().Format(dateLayout), + EndsAt: time.Now().Add(br.cfg.Am.SilenceDuration).UTC().Format(dateLayout), CreatedBy: "ntfy-alertmanager", Comment: "", Matchers: matchers, @@ -90,8 +90,8 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { } url := sb.AlertManagerURL - if br.cfg.am.URL != "" { - url = br.cfg.am.URL + if br.cfg.Am.URL != "" { + url = br.cfg.Am.URL } url += "/api/v2/silences" @@ -102,8 +102,8 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { } // Basic auth - if br.cfg.am.User != "" && br.cfg.am.Password != "" { - req.SetBasicAuth(br.cfg.am.User, br.cfg.am.Password) + if br.cfg.Am.User != "" && br.cfg.Am.Password != "" { + req.SetBasicAuth(br.cfg.Am.User, br.cfg.Am.Password) } req.Header.Add("Content-Type", "application/json")