config: Move to own package

This commit is contained in:
Thorben Günther 2023-07-12 14:56:48 +02:00
parent e66cc0d858
commit c70b82e9ab
No known key found for this signature in database
GPG key ID: 415CD778D8C5AFED
4 changed files with 113 additions and 105 deletions

View file

@ -1,4 +1,5 @@
package main // Package config defines the configuration file.
package config
import ( import (
"errors" "errors"
@ -9,32 +10,37 @@ import (
"git.sr.ht/~emersion/go-scfg" "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 ( const (
single alertMode = iota Single AlertMode = iota
multi Multi
) )
type cacheType int // CacheType is the type of cache that well be used.
type CacheType int
// The different types of caches.
const ( const (
memory cacheType = iota Memory CacheType = iota
redis Redis
) )
type config struct { // Config is the configuration of the bridge.
type Config struct {
BaseURL string BaseURL string
HTTPAddress string HTTPAddress string
LogLevel string LogLevel string
alertMode alertMode AlertMode AlertMode
User string User string
Password string Password string
ntfy ntfyConfig Ntfy ntfyConfig
labels labels Labels labels
cache cacheConfig Cache cacheConfig
am alertmanagerConfig Am alertmanagerConfig
resolved resolvedConfig Resolved resolvedConfig
} }
type ntfyConfig struct { type ntfyConfig struct {
@ -42,8 +48,8 @@ type ntfyConfig struct {
User string User string
Password string Password string
AccessToken string AccessToken string
emailAddress string EmailAddress string
call string Call string
} }
type labels struct { type labels struct {
@ -55,13 +61,13 @@ type labelConfig struct {
Priority string Priority string
Tags []string Tags []string
Icon string Icon string
emailAddress string EmailAddress string
call string Call string
} }
type cacheConfig struct { type cacheConfig struct {
// shared settings // shared settings
Type cacheType Type CacheType
Duration time.Duration Duration time.Duration
// memory settings // memory settings
CleanupInterval time.Duration CleanupInterval time.Duration
@ -81,24 +87,25 @@ type resolvedConfig struct {
Icon string 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) cfg, err := scfg.Load(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
config := new(config) config := new(Config)
// Set default values // Set default values
config.HTTPAddress = "127.0.0.1:8080" config.HTTPAddress = "127.0.0.1:8080"
config.LogLevel = "info" config.LogLevel = "info"
config.alertMode = single config.AlertMode = Single
config.cache.Type = memory config.Cache.Type = Memory
config.cache.Duration = time.Hour * 24 config.Cache.Duration = time.Hour * 24
// memory // memory
config.cache.CleanupInterval = time.Hour config.Cache.CleanupInterval = time.Hour
// redis // redis
config.cache.RedisURL = "redis://localhost:6379" config.Cache.RedisURL = "redis://localhost:6379"
d := cfg.Get("log-level") d := cfg.Get("log-level")
if d != nil { if d != nil {
@ -130,10 +137,10 @@ func readConfig(path string) (*config, error) {
switch strings.ToLower(mode) { switch strings.ToLower(mode) {
case "single": case "single":
config.alertMode = single config.AlertMode = Single
case "multi": case "multi":
config.alertMode = multi config.AlertMode = Multi
default: default:
return nil, fmt.Errorf("%q directive: illegal mode %q", d.Name, mode) 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 return nil, err
} }
config.labels.Order = strings.Split(order, ",") config.Labels.Order = strings.Split(order, ",")
} }
labels := make(map[string]labelConfig) labels := make(map[string]labelConfig)
for _, labelName := range config.labels.Order { for _, labelName := range config.Labels.Order {
for _, labelDir := range labelsDir.Children.GetAll(labelName) { for _, labelDir := range labelsDir.Children.GetAll(labelName) {
labelConfig := new(labelConfig) labelConfig := new(labelConfig)
var name string var name string
@ -207,14 +214,14 @@ func readConfig(path string) (*config, error) {
d = labelDir.Children.Get("email-address") d = labelDir.Children.Get("email-address")
if d != nil { if d != nil {
if err := d.ParseParams(&labelConfig.emailAddress); err != nil { if err := d.ParseParams(&labelConfig.EmailAddress); err != nil {
return nil, err return nil, err
} }
} }
d = labelDir.Children.Get("call") d = labelDir.Children.Get("call")
if d != nil { if d != nil {
if err := d.ParseParams(&labelConfig.call); err != nil { if err := d.ParseParams(&labelConfig.Call); err != nil {
return nil, err 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") ntfyDir := cfg.Get("ntfy")
@ -235,50 +242,50 @@ func readConfig(path string) (*config, error) {
if d == nil { if d == nil {
return nil, fmt.Errorf("%q missing from %q directive", "topic", "ntfy") 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 return nil, err
} }
d = ntfyDir.Children.Get("user") d = ntfyDir.Children.Get("user")
if d != nil { if d != nil {
if err := d.ParseParams(&config.ntfy.User); err != nil { if err := d.ParseParams(&config.Ntfy.User); err != nil {
return nil, err return nil, err
} }
} }
d = ntfyDir.Children.Get("password") d = ntfyDir.Children.Get("password")
if d != nil { if d != nil {
if err := d.ParseParams(&config.ntfy.Password); err != nil { if err := d.ParseParams(&config.Ntfy.Password); err != nil {
return nil, err return nil, err
} }
} }
if (config.ntfy.Password != "" && config.ntfy.User == "") || if (config.Ntfy.Password != "" && config.Ntfy.User == "") ||
(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") return nil, errors.New("ntfy: user and password have to be set together")
} }
d = ntfyDir.Children.Get("access-token") d = ntfyDir.Children.Get("access-token")
if d != nil { if d != nil {
if err := d.ParseParams(&config.ntfy.AccessToken); err != nil { if err := d.ParseParams(&config.Ntfy.AccessToken); err != nil {
return nil, err 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") 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") d = ntfyDir.Children.Get("email-address")
if d != nil { if d != nil {
if err := d.ParseParams(&config.ntfy.emailAddress); err != nil { if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil {
return nil, err return nil, err
} }
} }
d = ntfyDir.Children.Get("call") d = ntfyDir.Children.Get("call")
if d != nil { if d != nil {
if err := d.ParseParams(&config.ntfy.call); err != nil { if err := d.ParseParams(&config.Ntfy.Call); err != nil {
return nil, err return nil, err
} }
} }
@ -295,9 +302,9 @@ func readConfig(path string) (*config, error) {
switch strings.ToLower(cacheType) { switch strings.ToLower(cacheType) {
case "memory": case "memory":
config.cache.Type = memory config.Cache.Type = Memory
case "redis": case "redis":
config.cache.Type = redis config.Cache.Type = Redis
default: default:
return nil, fmt.Errorf("cache: illegal type %q", cacheType) return nil, fmt.Errorf("cache: illegal type %q", cacheType)
} }
@ -315,7 +322,7 @@ func readConfig(path string) (*config, error) {
return nil, err return nil, err
} }
config.cache.Duration = duration config.Cache.Duration = duration
} }
// memory // memory
@ -331,13 +338,13 @@ func readConfig(path string) (*config, error) {
return nil, err return nil, err
} }
config.cache.CleanupInterval = interval config.Cache.CleanupInterval = interval
} }
// redis // redis
d = cacheDir.Children.Get("redis-url") d = cacheDir.Children.Get("redis-url")
if d != nil { if d != nil {
if err := d.ParseParams(&config.cache.RedisURL); err != nil { if err := d.ParseParams(&config.Cache.RedisURL); err != nil {
return nil, err return nil, err
} }
} }
@ -358,31 +365,31 @@ func readConfig(path string) (*config, error) {
return nil, err return nil, err
} }
config.am.SilenceDuration = duration config.Am.SilenceDuration = duration
} }
d = amDir.Children.Get("user") d = amDir.Children.Get("user")
if d != nil { if d != nil {
if err := d.ParseParams(&config.am.User); err != nil { if err := d.ParseParams(&config.Am.User); err != nil {
return nil, err return nil, err
} }
} }
d = amDir.Children.Get("password") d = amDir.Children.Get("password")
if d != nil { if d != nil {
if err := d.ParseParams(&config.am.Password); err != nil { if err := d.ParseParams(&config.Am.Password); err != nil {
return nil, err return nil, err
} }
} }
if (config.am.Password != "" && config.am.User == "") || if (config.Am.Password != "" && config.Am.User == "") ||
(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") return nil, errors.New("alertmanager: user and password have to be set together")
} }
d = amDir.Children.Get("url") d = amDir.Children.Get("url")
if d != nil { if d != nil {
if err := d.ParseParams(&config.am.URL); err != nil { if err := d.ParseParams(&config.Am.URL); err != nil {
return nil, err return nil, err
} }
} }
@ -397,12 +404,12 @@ func readConfig(path string) (*config, error) {
return nil, err return nil, err
} }
config.resolved.Tags = strings.Split(tags, ",") config.Resolved.Tags = strings.Split(tags, ",")
} }
d = resolvedDir.Children.Get("icon") d = resolvedDir.Children.Get("icon")
if d != nil { if d != nil {
if err := d.ParseParams(&config.resolved.Icon); err != nil { if err := d.ParseParams(&config.Resolved.Icon); err != nil {
return nil, err return nil, err
} }
} }

View file

@ -1,4 +1,4 @@
package main package config
import ( import (
"os" "os"
@ -62,40 +62,40 @@ cache {
} }
` `
expectedCfg := &config{ expectedCfg := &Config{
BaseURL: "https://ntfy-alertmanager.xenrox.net", BaseURL: "https://ntfy-alertmanager.xenrox.net",
HTTPAddress: ":8080", HTTPAddress: ":8080",
LogLevel: "info", LogLevel: "info",
alertMode: multi, AlertMode: Multi,
User: "webhookUser", User: "webhookUser",
Password: "webhookPass", Password: "webhookPass",
ntfy: ntfyConfig{Topic: "https://ntfy.sh/alertmanager-alerts", User: "user", Password: "pass"}, Ntfy: ntfyConfig{Topic: "https://ntfy.sh/alertmanager-alerts", User: "user", Password: "pass"},
labels: labels{Order: []string{"severity", "instance"}, Labels: labels{Order: []string{"severity", "instance"},
Label: map[string]labelConfig{ Label: map[string]labelConfig{
"severity:critical": { "severity:critical": {
Priority: "5", Priority: "5",
Tags: []string{"rotating_light"}, Tags: []string{"rotating_light"},
Icon: "https://foo.com/critical.png", Icon: "https://foo.com/critical.png",
emailAddress: "foo@bar.com", EmailAddress: "foo@bar.com",
call: "yes", Call: "yes",
}, },
"severity:info": {Priority: "1"}, "severity:info": {Priority: "1"},
"instance:example.com": {Tags: []string{"computer", "example"}}, "instance:example.com": {Tags: []string{"computer", "example"}},
}, },
}, },
cache: cacheConfig{ Cache: cacheConfig{
Type: redis, Type: Redis,
Duration: 48 * time.Hour, Duration: 48 * time.Hour,
CleanupInterval: time.Hour, CleanupInterval: time.Hour,
RedisURL: "redis://user:password@localhost:6789/3", RedisURL: "redis://user:password@localhost:6789/3",
}, },
am: alertmanagerConfig{ Am: alertmanagerConfig{
SilenceDuration: time.Hour * 24, SilenceDuration: time.Hour * 24,
User: "user", User: "user",
Password: "pass", Password: "pass",
URL: "https://alertmanager.xenrox.net", URL: "https://alertmanager.xenrox.net",
}, },
resolved: resolvedConfig{ Resolved: resolvedConfig{
Tags: []string{"resolved", "partying_face"}, Tags: []string{"resolved", "partying_face"},
Icon: "https://foo.com/resolved.png", Icon: "https://foo.com/resolved.png",
}, },
@ -107,7 +107,7 @@ cache {
t.Errorf("failed to write config file: %v", err) t.Errorf("failed to write config file: %v", err)
} }
cfg, err := readConfig(configPath) cfg, err := ReadConfig(configPath)
if err != nil { if err != nil {
t.Errorf("failed to read config file: %v", err) t.Errorf("failed to read config file: %v", err)
} }

67
main.go
View file

@ -16,6 +16,7 @@ import (
"git.xenrox.net/~xenrox/go-log" "git.xenrox.net/~xenrox/go-log"
"git.xenrox.net/~xenrox/ntfy-alertmanager/cache" "git.xenrox.net/~xenrox/ntfy-alertmanager/cache"
"git.xenrox.net/~xenrox/ntfy-alertmanager/config"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@ -23,7 +24,7 @@ import (
var version = "dev" var version = "dev"
type bridge struct { type bridge struct {
cfg *config cfg *config.Config
logger *log.Logger logger *log.Logger
cache cache.Cache cache cache.Cache
client *httpClient client *httpClient
@ -102,20 +103,20 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification {
var tags []string var tags []string
if alert.Status == "resolved" { if alert.Status == "resolved" {
tags = append(tags, br.cfg.resolved.Tags...) tags = append(tags, br.cfg.Resolved.Tags...)
n.icon = br.cfg.resolved.Icon n.icon = br.cfg.Resolved.Icon
} }
n.emailAddress = br.cfg.ntfy.emailAddress n.emailAddress = br.cfg.Ntfy.EmailAddress
n.call = br.cfg.ntfy.call 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] val, ok := alert.Labels[labelName]
if !ok { if !ok {
continue 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 { if !ok {
continue continue
} }
@ -129,11 +130,11 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification {
} }
if n.emailAddress == "" { if n.emailAddress == "" {
n.emailAddress = labelConfig.emailAddress n.emailAddress = labelConfig.EmailAddress
} }
if n.call == "" { if n.call == "" {
n.call = labelConfig.call n.call = labelConfig.Call
} }
for _, val := range labelConfig.Tags { for _, val := range labelConfig.Tags {
@ -145,7 +146,7 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification {
n.tags = strings.Join(tags, ",") 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 == "" { if br.cfg.BaseURL == "" {
br.logger.Error("Failed to create silence action: No base-url set") br.logger.Error("Failed to create silence action: No base-url set")
} else { } else {
@ -210,20 +211,20 @@ func (br *bridge) multiAlertNotification(p *payload) *notification {
var tags []string var tags []string
if p.Status == "resolved" { if p.Status == "resolved" {
tags = append(tags, br.cfg.resolved.Tags...) tags = append(tags, br.cfg.Resolved.Tags...)
n.icon = br.cfg.resolved.Icon n.icon = br.cfg.Resolved.Icon
} }
n.emailAddress = br.cfg.ntfy.emailAddress n.emailAddress = br.cfg.Ntfy.EmailAddress
n.call = br.cfg.ntfy.call 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] val, ok := p.CommonLabels[labelName]
if !ok { if !ok {
continue 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 { if !ok {
continue continue
} }
@ -237,11 +238,11 @@ func (br *bridge) multiAlertNotification(p *payload) *notification {
} }
if n.emailAddress == "" { if n.emailAddress == "" {
n.emailAddress = labelConfig.emailAddress n.emailAddress = labelConfig.EmailAddress
} }
if n.call == "" { if n.call == "" {
n.call = labelConfig.call n.call = labelConfig.Call
} }
for _, val := range labelConfig.Tags { for _, val := range labelConfig.Tags {
@ -253,7 +254,7 @@ func (br *bridge) multiAlertNotification(p *payload) *notification {
n.tags = strings.Join(tags, ",") 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 == "" { if br.cfg.BaseURL == "" {
br.logger.Error("Failed to create silence action: No base-url set") br.logger.Error("Failed to create silence action: No base-url set")
} else { } else {
@ -272,16 +273,16 @@ func (br *bridge) multiAlertNotification(p *payload) *notification {
} }
func (br *bridge) publish(n *notification) error { 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 { if err != nil {
return err return err
} }
// ntfy authentication // ntfy authentication
if br.cfg.ntfy.Password != "" && br.cfg.ntfy.User != "" { if br.cfg.Ntfy.Password != "" && br.cfg.Ntfy.User != "" {
req.SetBasicAuth(br.cfg.ntfy.User, br.cfg.ntfy.Password) req.SetBasicAuth(br.cfg.Ntfy.User, br.cfg.Ntfy.Password)
} else if br.cfg.ntfy.AccessToken != "" { } else if br.cfg.Ntfy.AccessToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", br.cfg.ntfy.AccessToken)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", br.cfg.Ntfy.AccessToken))
} }
req.Header.Set("X-Title", n.title) 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) br.logger.Debugf("Received alert %+v", event)
} }
if br.cfg.alertMode == single { if br.cfg.AlertMode == config.Single {
notifications := br.singleAlertNotifications(&event) notifications := br.singleAlertNotifications(&event)
for _, n := range notifications { for _, n := range notifications {
err := br.publish(n) err := br.publish(n)
@ -412,7 +413,7 @@ func (br *bridge) basicAuthMiddleware(handler http.HandlerFunc) http.HandlerFunc
func (br *bridge) runCleanup() { func (br *bridge) runCleanup() {
for { for {
time.Sleep(br.cfg.cache.CleanupInterval) time.Sleep(br.cfg.Cache.CleanupInterval)
br.logger.Info("Pruning cache") br.logger.Info("Pruning cache")
br.cache.Cleanup() br.cache.Cleanup()
} }
@ -432,7 +433,7 @@ func main() {
logger := log.NewDefaultLogger() logger := log.NewDefaultLogger()
cfg, err := readConfig(configPath) cfg, err := config.ReadConfig(configPath)
if err != nil { if err != nil {
logger.Fatalf("Failed to read config: %v", err) logger.Fatalf("Failed to read config: %v", err)
} }
@ -444,12 +445,12 @@ func main() {
client := &httpClient{&http.Client{Timeout: time.Second * 3}} client := &httpClient{&http.Client{Timeout: time.Second * 3}}
var c cache.Cache var c cache.Cache
switch cfg.cache.Type { switch cfg.Cache.Type {
case memory: case config.Memory:
c = cache.NewMemoryCache(cfg.cache.Duration) c = cache.NewMemoryCache(cfg.Cache.Duration)
case redis: case config.Redis:
var err error 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 { if err != nil {
logger.Fatalf("Failed to create redis cache: %v", err) logger.Fatalf("Failed to create redis cache: %v", err)
} }
@ -467,7 +468,7 @@ func main() {
http.HandleFunc("/silences", bridge.handleSilences) http.HandleFunc("/silences", bridge.handleSilences)
} }
if cfg.cache.Type == memory { if cfg.Cache.Type == config.Memory {
go bridge.runCleanup() go bridge.runCleanup()
} }
logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil)) logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil))

View file

@ -77,7 +77,7 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
silence := &silence{ silence := &silence{
StartsAt: time.Now().UTC().Format(dateLayout), 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", CreatedBy: "ntfy-alertmanager",
Comment: "", Comment: "",
Matchers: matchers, Matchers: matchers,
@ -90,8 +90,8 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
} }
url := sb.AlertManagerURL url := sb.AlertManagerURL
if br.cfg.am.URL != "" { if br.cfg.Am.URL != "" {
url = br.cfg.am.URL url = br.cfg.Am.URL
} }
url += "/api/v2/silences" url += "/api/v2/silences"
@ -102,8 +102,8 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
} }
// Basic auth // Basic auth
if 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.SetBasicAuth(br.cfg.Am.User, br.cfg.Am.Password)
} }
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")