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 (
"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
}
}

View file

@ -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)
}

67
main.go
View file

@ -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))

View file

@ -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")