chore(backend): switch to github.com/knadh/koanf for config handling

This commit is contained in:
Łukasz Mierzwa
2020-02-03 18:42:08 +00:00
parent 3af58601df
commit 05e9ab5299
14 changed files with 256 additions and 182 deletions

View File

@@ -0,0 +1,14 @@
# Load 'karma.yaml' from cwd by default
karma.bin-should-work --check-config
! stdout .
stderr 'msg="Reading configuration file karma.yaml"'
stderr 'msg="Configuration is valid"'
stderr 'msg="\[cwd\] Configured Alertmanager source at http://localhost:8080 \(proxied: true\, readonly: false\)"'
! stderr 'level=error'
-- karma.yaml --
alertmanager:
servers:
- name: cwd
uri: "http://localhost:8080"
proxy: true

View File

@@ -1,7 +1,7 @@
# Raises an error if we pass alertmanager.interval value that doesn't parse
karma.bin-should-fail-no-timestamp --log.format=text --log.config=false --log.level=error --config.file karma.yaml
! stdout .
stderr 'level=fatal msg="yaml: unmarshal errors:\\n line 2: cannot unmarshal !!str `abc123` into time.Duration"'
stderr 'level=fatal msg=".* invalid duration abc123"'
-- karma.yaml --
alertmanager:

View File

@@ -1,9 +1,9 @@
# Print out and compare logged config set via config file
karma.bin-should-work --config.file=karma.yaml --check-config
karma.bin-should-work --config.file=custom.yaml --check-config
! stdout .
cmp stderr expected.stderr
-- karma.yaml --
-- custom.yaml --
alertmanager:
interval: 10s
servers:
@@ -224,7 +224,7 @@ FLR1flnW2lx5o5csDzTpi+jgC6nu1zE0DWo1c5ZdpVO289POIpqh
-----END RSA PRIVATE KEY-----
-- expected.stderr --
level=info msg="Reading configuration file karma.yaml"
level=info msg="Reading configuration file custom.yaml"
level=info msg="Version: dev"
level=info msg="Parsed configuration:"
level=info msg="alertmanager:"

View File

@@ -57,8 +57,8 @@ ui:
collapseGroups: collapsedOanMobile
-- expected.stderr --
level=fatal msg="4 error(s) decoding:\n\n* '[2].Headers[0]' expected a map, got 'string'\n* cannot parse '[0].Proxy' as bool: strconv.ParseBool: parsing \"YEs\": invalid syntax\n* error decoding '[0].Timeout': time: invalid duration bbb\n* error decoding '[2].Timeout': time: invalid duration z"
level=fatal msg="Invalid ui.collapseGroups value 'collapsedOanMobile', allowed options: expanded, collapsed, collapsedOnMobile"
level=fatal msg="Invalid ui.theme value 'x', allowed options: light, dark, auto"
level=fatal msg="yaml: unmarshal errors:\n line 2: cannot unmarshal !!str `jjs88` into time.Duration\n line 6: cannot unmarshal !!str `bbb` into time.Duration\n line 7: cannot unmarshal !!str `YEs` into bool\n line 11: cannot unmarshal !!int `1` into bool\n line 14: cannot unmarshal !!str `z` into time.Duration\n line 16: cannot unmarshal !!int `0` into bool\n line 18: cannot unmarshal !!seq into map[string]string\n line 27: cannot unmarshal !!str `zzz` into bool\n line 33: cannot unmarshal !!str `z` into bool\n line 34: cannot unmarshal !!map into []string\n line 45: cannot unmarshal !!str `10sm` into time.Duration\n line 46: cannot unmarshal !!str `z` into bool\n line 47: cannot unmarshal !!str `yum` into bool\n line 49: cannot unmarshal !!str `abc4` into int\n line 50: cannot unmarshal !!str `5a` into int"
level=error msg="Unknown log level '123'"
level=fatal msg="Failed to unmarshal configuration: 12 error(s) decoding:\n\n* 'Alertmanager.Servers[2].Headers[0]' expected a map, got 'string'\n* cannot parse 'Alertmanager.Servers[0].Proxy' as bool: strconv.ParseBool: parsing \"YEs\": invalid syntax\n* cannot parse 'Annotations.Default.Hidden' as bool: strconv.ParseBool: parsing \"z\": invalid syntax\n* cannot parse 'UI.alertsPerGroup' as int: strconv.ParseInt: parsing \"5a\": invalid syntax\n* cannot parse 'UI.colorTitlebar' as bool: strconv.ParseBool: parsing \"yum\": invalid syntax\n* cannot parse 'UI.hideFiltersWhenIdle' as bool: strconv.ParseBool: parsing \"z\": invalid syntax\n* cannot parse 'UI.minimalGroupWidth' as int: strconv.ParseInt: parsing \"abc4\": invalid syntax\n* cannot parse 'alertAcknowledgement.Enabled' as bool: strconv.ParseBool: parsing \"zzz\": invalid syntax\n* error decoding 'Alertmanager.Interval': time: invalid duration jjs88\n* error decoding 'Alertmanager.Servers[0].Timeout': time: invalid duration bbb\n* error decoding 'Alertmanager.Servers[2].Timeout': time: invalid duration z\n* error decoding 'UI.Refresh': time: unknown unit sm in duration 10sm"
level=fatal msg="Invalid grid.sorting.order value '', allowed options: disabled, startsAt, label"
level=fatal msg="Invalid ui.collapseGroups value '', allowed options: expanded, collapsed, collapsedOnMobile"
level=fatal msg="Invalid ui.theme value '', allowed options: light, dark, auto"
level=error msg="Unknown log level ''"

View File

@@ -0,0 +1,4 @@
# Errors when config.file points to missing file
karma.bin-should-fail --config.file=404.yaml
! stdout .
stderr 'level=fatal msg="Failed to load configuration file \\"404.yaml\\": open 404.yaml: no such file or directory'

View File

@@ -0,0 +1,15 @@
# Raises an error if silence form populate from header config is using invalid regex rule
karma.bin-should-fail --log.format=text --log.config=false --log.level=error --config.file=karma.yaml
! stdout .
stderr 'msg="Invalid regex for silenceform.author.populate_from_header.value_re: error parsing regexp: invalid nested repetition operator: `\+\+`"'
-- karma.yaml --
alertmanager:
servers:
- name: default
uri: https://localhost:9093
silenceForm:
author:
populate_from_header:
header: "CF-RAY"
value_re: ".++++"

View File

@@ -0,0 +1,14 @@
# Raises an error if silence form populate from header config is missing header name
karma.bin-should-fail --log.format=text --log.config=false --log.level=error --config.file=karma.yaml
! stdout .
stderr 'msg="silenceform.author.populate_from_header.header is required when silenceform.author.populate_from_header.value_re is set"'
-- karma.yaml --
alertmanager:
servers:
- name: default
uri: https://localhost:9093
silenceForm:
author:
populate_from_header:
value_re: "^(.+)$"

View File

@@ -0,0 +1,14 @@
# Raises an error if silence form populate from header config is missing regex rule
karma.bin-should-fail --log.format=text --log.config=false --log.level=error --config.file=karma.yaml
! stdout .
stderr 'msg="silenceform.author.populate_from_header.value_re is required when silenceform.author.populate_from_header.header is set"'
-- karma.yaml --
alertmanager:
servers:
- name: default
uri: https://localhost:9093
silenceForm:
author:
populate_from_header:
header: "CF-RAY"

4
go.mod
View File

@@ -24,6 +24,8 @@ require (
github.com/google/go-cmp v0.4.0
github.com/hansrodtang/randomcolor v0.0.0-20160512071917-d27108b3d7a5
github.com/jarcoal/httpmock v1.0.4
github.com/knadh/koanf v0.8.0
github.com/mitchellh/mapstructure v1.1.2
github.com/patrickmn/go-cache v2.1.1-0.20180815053127-5633e0862627+incompatible
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_golang v1.4.1
@@ -31,7 +33,7 @@ require (
github.com/rogpeppe/go-internal v1.5.2
github.com/sirupsen/logrus v1.4.2
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.6.2
github.com/spf13/viper v1.6.2 // indirect
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect
gopkg.in/go-playground/colors.v1 v1.2.0
gopkg.in/yaml.v2 v2.2.8

8
go.sum
View File

@@ -59,6 +59,7 @@ github.com/elazarl/go-bindata-assetfs v1.0.1-0.20180223160309-38087fe4dafb h1:Dn
github.com/elazarl/go-bindata-assetfs v1.0.1-0.20180223160309-38087fe4dafb/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
@@ -184,6 +185,7 @@ github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUD
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw=
@@ -280,6 +282,8 @@ github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/knadh/koanf v0.8.0 h1:j+851fIYrSzmdCn4jbiwU247qwMDIhyWXRJs74ZCB+8=
github.com/knadh/koanf v0.8.0/go.mod h1:kVvmDbXnBtW49Czi4c1M+nnOWF0YSNZ8BaKvE/bCO1w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -345,6 +349,8 @@ github.com/patrickmn/go-cache v2.1.1-0.20180815053127-5633e0862627+incompatible/
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -371,6 +377,7 @@ github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLk
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -512,6 +519,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -3,7 +3,6 @@ package config
import (
"bufio"
"bytes"
"io/ioutil"
"os"
"regexp"
"strings"
@@ -11,8 +10,15 @@ import (
"github.com/prymitive/karma/internal/slices"
"github.com/prymitive/karma/internal/uri"
"github.com/knadh/koanf"
yamlParser "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
"github.com/mitchellh/mapstructure"
"github.com/spf13/pflag"
"github.com/spf13/viper"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
@@ -58,7 +64,7 @@ func SetupFlags(f *pflag.FlagSet) {
"List of annotations to keep, all other annotations will be stripped")
f.StringSlice("annotations.strip", []string{}, "List of annotations to ignore")
f.String("config.file", "", "Full path to the configuration file")
f.String("config.file", "", "Full path to the configuration file, 'karma.yaml' will be used if found in the current working directory")
f.String("custom.css", "", "Path to a file with custom CSS to load")
f.String("custom.js", "", "Path to a file with custom JavaScript to load")
@@ -111,126 +117,132 @@ func SetupFlags(f *pflag.FlagSet) {
f.String("ui.collapseGroups", "collapsedOnMobile", "Default state for alert groups")
}
// ReadConfig will read all sources of configuration, merge all keys and
// populate global Config variable, it should be only called on startup
func (config *configSchema) Read(flags *pflag.FlagSet) string {
v := viper.New()
err := v.BindPFlags(flags)
if err != nil {
log.Errorf("Failed to bind flag set to the configuration: %s", err)
}
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// special envs
// HOST and PORT is used by gin
err = v.BindEnv("listen.address", "HOST")
if err != nil {
log.Errorf("Failed to bind listen.address config key to the HOST env variable: %s", err)
}
err = v.BindEnv("listen.port", "PORT")
if err != nil {
log.Errorf("Failed to bind listen.port config key to the PORT env variable: %s", err)
}
// raven-go expects this
err = v.BindEnv("sentry.private", "SENTRY_DSN")
if err != nil {
log.Errorf("Failed to bind sentry.private config key to the SENTRY_DSN env variable: %s", err)
}
v.SetConfigType("yaml")
configFile := v.GetString("config.file")
// if config file is not set then try loading karma.yaml from current directory
func readConfigFile(k *koanf.Koanf, flags *pflag.FlagSet) string {
configFile, _ := flags.GetString("config.file")
// if config.file is not passed via flags then see if there's karma.yaml in
// current working directory
if configFile == "" {
if _, err = os.Stat("karma.yaml"); !os.IsNotExist(err) {
if _, err := os.Stat("karma.yaml"); !os.IsNotExist(err) {
configFile = "karma.yaml"
}
}
if configFile != "" {
v.SetConfigFile(configFile)
if err := k.Load(file.Provider(configFile), yamlParser.Parser()); err != nil {
log.Fatalf("Failed to load configuration file %q: %v", configFile, err)
}
return configFile
}
return configFile
}
func readEnvVariables(k *koanf.Koanf) {
customEnvs := map[string]string{
"HOST": "listen.address",
"PORT": "listen.port",
"SENTRY_DSN": "sentry.private",
}
for env, key := range customEnvs {
if _, found := os.LookupEnv(env); found {
_ = k.Load(confmap.Provider(map[string]interface{}{
key: os.Getenv(env),
}, "."), nil)
}
}
err = v.ReadInConfig()
if v.ConfigFileUsed() != "" && err != nil {
log.Fatal(err)
}
_ = k.Load(env.Provider("", ".", func(s string) string {
switch s {
case "ALERTMANAGER_EXTERNAL_URI":
return "alertmanager.external_uri"
case "ALERTACKNOWLEDGEMENT_ENABLED":
return "alertAcknowledgement.enabled"
case "ALERTACKNOWLEDGEMENT_DURATION":
return "alertAcknowledgement.duration"
case "ALERTACKNOWLEDGEMENT_AUTHOR":
return "alertAcknowledgement.author"
case "ALERTACKNOWLEDGEMENT_COMMENTPREFIX":
return "alertAcknowledgement.commentPrefix"
case "SILENCEFORM_AUTHOR_POPULATE_FROM_HEADER_HEADER":
return "silenceForm.author.populate_from_header.header"
case "SILENCEFORM_AUTHOR_POPULATE_FROM_HEADER_VALUE_RE":
return "silenceForm.author.populate_from_header.value_re"
case "SILENCEFORM_STRIP_LABELS":
return "silenceForm.strip.labels"
case "UI_HIDEFILTERSWHENIDLE":
return "ui.hideFiltersWhenIdle"
case "UI_COLORTITLEBAR":
return "ui.colorTitlebar"
case "UI_MINIMALGROUPWIDTH":
return "ui.minimalGroupWidth"
case "UI_ALERTSPERGROUP":
return "ui.alertsPerGroup"
case "UI_COLLAPSEGROUPS":
return "ui.collapseGroups"
default:
return strings.Replace(strings.ToLower(s), "_", ".", -1)
}
}), nil)
}
config.Alertmanager.Servers = []AlertmanagerConfig{}
config.Alertmanager.Interval = v.GetDuration("alertmanager.interval")
config.AlertAcknowledgement.Enabled = v.GetBool("alertAcknowledgement.enabled")
config.AlertAcknowledgement.Author = v.GetString("alertAcknowledgement.author")
config.AlertAcknowledgement.CommentPrefix = v.GetString("alertAcknowledgement.commentPrefix")
config.AlertAcknowledgement.Duration = v.GetDuration("alertAcknowledgement.duration")
config.Annotations.Default.Hidden = v.GetBool("annotations.default.hidden")
config.Annotations.Hidden = v.GetStringSlice("annotations.hidden")
config.Annotations.Visible = v.GetStringSlice("annotations.visible")
config.Annotations.Keep = v.GetStringSlice("annotations.keep")
config.Annotations.Strip = v.GetStringSlice("annotations.strip")
config.Custom.CSS = v.GetString("custom.css")
config.Custom.JS = v.GetString("custom.js")
config.Debug = v.GetBool("debug")
config.Filters.Default = v.GetStringSlice("filters.default")
config.Grid.Sorting.Order = v.GetString("grid.sorting.order")
config.Grid.Sorting.Reverse = v.GetBool("grid.sorting.reverse")
config.Grid.Sorting.Label = v.GetString("grid.sorting.label")
config.Karma.Name = v.GetString("karma.name")
config.Labels.Color.Custom = CustomLabelColors{}
config.Labels.Color.Static = v.GetStringSlice("labels.color.static")
config.Labels.Color.Unique = v.GetStringSlice("labels.color.unique")
config.Labels.Keep = v.GetStringSlice("labels.keep")
config.Labels.Strip = v.GetStringSlice("labels.strip")
config.Listen.Address = v.GetString("listen.address")
config.Listen.Port = v.GetInt("listen.port")
config.Listen.Prefix = v.GetString("listen.prefix")
config.Log.Config = v.GetBool("log.config")
config.Log.Level = v.GetString("log.level")
config.Log.Format = v.GetString("log.format")
config.Log.Timestamp = v.GetBool("log.timestamp")
config.Receivers.Keep = v.GetStringSlice("receivers.keep")
config.Receivers.Strip = v.GetStringSlice("receivers.strip")
config.Sentry.Private = v.GetString("sentry.private")
config.Sentry.Public = v.GetString("sentry.public")
config.SilenceForm.Strip.Labels = v.GetStringSlice("silenceform.strip.labels")
config.SilenceForm.Author.PopulateFromHeader.Header = v.GetString("silenceform.author.populate_from_header.header")
config.SilenceForm.Author.PopulateFromHeader.ValueRegex = v.GetString("silenceform.author.populate_from_header.value_re")
config.UI.Refresh = v.GetDuration("ui.refresh")
config.UI.HideFiltersWhenIdle = v.GetBool("ui.hideFiltersWhenIdle")
config.UI.ColorTitlebar = v.GetBool("ui.colorTitlebar")
config.UI.Theme = v.GetString("ui.theme")
config.UI.MinimalGroupWidth = v.GetInt("ui.minimalGroupWidth")
config.UI.AlertsPerGroup = v.GetInt("ui.alertsPerGroup")
config.UI.CollapseGroups = v.GetString("ui.collapseGroups")
func readFlags(k *koanf.Koanf, flags *pflag.FlagSet) {
_ = k.Load(posflag.Provider(flags, ".", k), nil)
}
// ReadConfig will read all sources of configuration, merge all keys and
// populate global Config variable, it should be only called on startup
// Order in which we read configuration:
// 1. CLI flags
// 2. Config file
// 3. Environment variables
func (config *configSchema) Read(flags *pflag.FlagSet) string {
k := koanf.New(".")
var configFileUsed string
// 3. read all environemnt variables
readEnvVariables(k)
// 2. read config file
if cf := readConfigFile(k, flags); cf != "" {
configFileUsed = cf
}
// 1. read flags
readFlags(k, flags)
dConf := mapstructure.DecoderConfig{
Result: &config,
WeaklyTypedInput: true,
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToSliceHookFunc(" "),
mapstructure.StringToTimeDurationHookFunc(),
),
ZeroFields: true,
}
kConf := koanf.UnmarshalConf{
Tag: "koanf",
FlatPaths: false,
DecoderConfig: &dConf,
}
err := k.UnmarshalWithConf("", &config, kConf)
if err != nil {
log.Fatalf("Failed to unmarshal configuration: %v", err)
}
if config.SilenceForm.Author.PopulateFromHeader.ValueRegex != "" {
_, err = regexp.Compile(config.SilenceForm.Author.PopulateFromHeader.ValueRegex)
if err != nil {
log.Fatalf("Invalid regex for silenceform.author.populate_from_header.value_re: %s", err.Error())
}
if config.SilenceForm.Author.PopulateFromHeader.Header == "" {
log.Fatalf("silenceform.author.populate_from_header.header is required when silenceform.author.populate_from_header.value_re is set")
}
} else if config.SilenceForm.Author.PopulateFromHeader.Header != "" {
log.Fatalf("silenceform.author.populate_from_header.value_re is required when silenceform.author.populate_from_header.header is set")
}
err = v.UnmarshalKey("alertmanager.servers", &config.Alertmanager.Servers)
if err != nil {
log.Fatal(err)
}
for i, s := range config.Alertmanager.Servers {
if s.Timeout.Seconds() == 0 {
config.Alertmanager.Servers[i].Timeout = v.GetDuration("alertmanager.timeout")
config.Alertmanager.Servers[i].Timeout = config.Alertmanager.Timeout
}
}
err = v.UnmarshalKey("silences.comments.linkDetect.rules", &config.Silences.Comments.LinkDetect.Rules)
if err != nil {
log.Fatal(err)
}
err = v.UnmarshalKey("labels.color.custom", &config.Labels.Color.Custom)
if err != nil {
log.Fatal(err)
}
for labelName, customColors := range config.Labels.Color.Custom {
for i, customColor := range customColors {
if customColor.Value == "" && customColor.ValueRegex == "" {
@@ -245,11 +257,6 @@ func (config *configSchema) Read(flags *pflag.FlagSet) string {
}
}
err = v.UnmarshalKey("grid.sorting.customValues.labels", &config.Grid.Sorting.CustomValues.Labels)
if err != nil {
log.Fatal(err)
}
if !slices.StringInSlice([]string{"disabled", "startsAt", "label"}, config.Grid.Sorting.Order) {
log.Fatalf("Invalid grid.sorting.order value '%s', allowed options: disabled, startsAt, label", config.Grid.Sorting.Order)
}
@@ -262,43 +269,24 @@ func (config *configSchema) Read(flags *pflag.FlagSet) string {
log.Fatalf("Invalid ui.theme value '%s', allowed options: light, dark, auto", config.UI.Theme)
}
// FIXME workaround for https://github.com/prymitive/karma/issues/507
// until https://github.com/spf13/viper/pull/635 is merged
// read in raw config file if it's used and override maps where keys are label
// names so we can't enforce parsed config key names
if v.ConfigFileUsed() != "" {
raw := configSchema{}
var rawConfigFile []byte
rawConfigFile, err = ioutil.ReadFile(v.ConfigFileUsed())
if err != nil {
log.Fatal(err)
}
err = yaml.Unmarshal(rawConfigFile, &raw)
if err != nil {
log.Fatal(err)
}
config.Grid.Sorting.CustomValues.Labels = raw.Grid.Sorting.CustomValues.Labels
}
// accept single Alertmanager server from flag/env if nothing is set yet
if len(config.Alertmanager.Servers) == 0 && v.GetString("alertmanager.uri") != "" {
if len(config.Alertmanager.Servers) == 0 && config.Alertmanager.URI != "" {
config.Alertmanager.Servers = []AlertmanagerConfig{
{
Name: v.GetString("alertmanager.name"),
URI: v.GetString("alertmanager.uri"),
ExternalURI: v.GetString("alertmanager.external_uri"),
Timeout: v.GetDuration("alertmanager.timeout"),
Proxy: v.GetBool("alertmanager.proxy"),
ReadOnly: v.GetBool("alertmanager.readonly"),
Name: config.Alertmanager.Name,
URI: config.Alertmanager.URI,
ExternalURI: config.Alertmanager.ExternalURI,
Timeout: config.Alertmanager.Timeout,
Proxy: config.Alertmanager.Proxy,
ReadOnly: config.Alertmanager.ReadOnly,
Headers: make(map[string]string),
},
}
}
return v.ConfigFileUsed()
Config = *config
return configFileUsed
}
// LogValues will dump runtime config to logs
@@ -328,11 +316,7 @@ func (config *configSchema) LogValues() {
config.Sentry.Private = uri.SanitizeURI(config.Sentry.Private)
}
out, err := yaml.Marshal(cfg)
if err != nil {
log.Error(err)
}
out, _ := yaml.Marshal(cfg)
log.Info("Parsed configuration:")
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {

View File

@@ -209,6 +209,7 @@ func TestReadSimpleConfig(t *testing.T) {
resetEnv()
log.SetLevel(log.ErrorLevel)
os.Setenv("ALERTMANAGER_URI", "http://localhost")
os.Setenv("ALERTMANAGER_EXTERNAL_URI", "http://localhost:9090")
os.Setenv("ALERTMANAGER_NAME", "single")
os.Setenv("ALERTMANAGER_TIMEOUT", "15s")
os.Setenv("ALERTMANAGER_PROXY", "true")
@@ -218,6 +219,12 @@ func TestReadSimpleConfig(t *testing.T) {
t.Errorf("Expected 1 Alertmanager server, got %d", len(Config.Alertmanager.Servers))
} else {
am := Config.Alertmanager.Servers[0]
if am.URI != "http://localhost" {
t.Errorf("Expect Alertmanager URI 'http://localhost' got '%s'", am.URI)
}
if am.ExternalURI != "http://localhost:9090" {
t.Errorf("Expect Alertmanager external_uri 'http://localhost:9090' got '%s'", am.ExternalURI)
}
if am.Name != "single" {
t.Errorf("Expect Alertmanager name 'single' got '%s'", am.Name)
}
@@ -227,6 +234,9 @@ func TestReadSimpleConfig(t *testing.T) {
if Config.Alertmanager.Interval != time.Minute*3 {
t.Errorf("Expect Alertmanager timeout '%v' got '%v'", time.Minute*3, Config.Alertmanager.Interval)
}
if am.Proxy != true {
t.Errorf("Expect Alertmanager proxy 'true' got '%v'", am.Proxy)
}
}
}
@@ -284,6 +294,9 @@ func TestInvalidSilenceFormRegex(t *testing.T) {
mockConfigRead()
if Config.SilenceForm.Author.PopulateFromHeader.ValueRegex != ".****" {
t.Errorf("Config.SilenceForm.Author.PopulateFromHeader.ValueRegex value is %q", Config.SilenceForm.Author.PopulateFromHeader.ValueRegex)
}
if !wasFatal {
t.Error("Invalid silence form regex didn't cause log.Fatal()")
}

View File

@@ -8,44 +8,50 @@ import (
type AlertmanagerConfig struct {
Name string
URI string
ExternalURI string `yaml:"external_uri" mapstructure:"external_uri"`
ExternalURI string `yaml:"external_uri" koanf:"external_uri"`
Timeout time.Duration
Proxy bool
ReadOnly bool `yaml:"readonly" mapstructure:"readonly"`
ReadOnly bool `yaml:"readonly"`
TLS struct {
CA string
Cert string
Key string
InsecureSkipVerify bool `yaml:"insecureSkipVerify" mapstructure:"insecureSkipVerify"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify" koanf:"insecureSkipVerify"`
}
Headers map[string]string
}
type LinkDetectRules struct {
Regex string `yaml:"regex" mapstructure:"regex"`
URITemplate string `yaml:"uriTemplate" mapstructure:"uriTemplate"`
Regex string `yaml:"regex"`
URITemplate string `yaml:"uriTemplate" koanf:"uriTemplate"`
}
type CustomLabelColor struct {
Value string `yaml:"value" mapstructure:"value"`
ValueRegex string `yaml:"value_re" mapstructure:"value_re"`
CompiledRegex *regexp.Regexp `yaml:"-" mapstructure:"-"`
Color string `yaml:"color" mapstructure:"color"`
Value string `yaml:"value"`
ValueRegex string `yaml:"value_re" koanf:"value_re"`
CompiledRegex *regexp.Regexp `yaml:"-"`
Color string `yaml:"color"`
}
type CustomLabelColors map[string][]CustomLabelColor
type configSchema struct {
Alertmanager struct {
Interval time.Duration
Servers []AlertmanagerConfig
Interval time.Duration
Servers []AlertmanagerConfig
Name string `yaml:"-" koanf:"name"`
Timeout time.Duration `yaml:"-" koanf:"timeout"`
URI string `yaml:"-" koanf:"uri"`
ExternalURI string `yaml:"-" koanf:"external_uri"`
Proxy bool `yaml:"-" koanf:"proxy"`
ReadOnly bool `yaml:"-" koanf:"readonly"`
}
AlertAcknowledgement struct {
Enabled bool
Duration time.Duration
Author string
CommentPrefix string `yaml:"commentPrefix" mapstructure:"commentPrefix"`
} `yaml:"alertAcknowledgement" mapstructure:"alertAcknowledgement"`
CommentPrefix string `yaml:"commentPrefix" koanf:"commentPrefix"`
} `yaml:"alertAcknowledgement" koanf:"alertAcknowledgement"`
Annotations struct {
Default struct {
Hidden bool
@@ -70,9 +76,9 @@ type configSchema struct {
Label string
CustomValues struct {
Labels map[string]map[string]string
} `yaml:"customValues" mapstructure:"customValues"`
} `yaml:"customValues" koanf:"customValues"`
}
}
} `yaml:"grid"`
Karma struct {
Name string
}
@@ -107,28 +113,28 @@ type configSchema struct {
Silences struct {
Comments struct {
LinkDetect struct {
Rules []LinkDetectRules `yaml:"rules" mapstructure:"rules"`
} `yaml:"linkDetect" mapstructure:"linkDetect"`
} `yaml:"comments" mapstructure:"comments"`
} `yaml:"silences" mapstructure:"silences"`
Rules []LinkDetectRules `yaml:"rules"`
} `yaml:"linkDetect" koanf:"linkDetect"`
} `yaml:"comments"`
} `yaml:"silences"`
SilenceForm struct {
Author struct {
PopulateFromHeader struct {
Header string `yaml:"header" mapstructure:"header"`
ValueRegex string `yaml:"value_re" mapstructure:"value_re"`
} `yaml:"populate_from_header" mapstructure:"populate_from_header"`
} `yaml:"author" mapstructure:"author"`
Header string `yaml:"header" koanf:"header"`
ValueRegex string `yaml:"value_re" koanf:"value_re"`
} `yaml:"populate_from_header" koanf:"populate_from_header"`
} `yaml:"author"`
Strip struct {
Labels []string
}
} `yaml:"silenceForm" mapstructure:"silenceForm"`
} `yaml:"silenceForm" koanf:"silenceForm"`
UI struct {
Refresh time.Duration
HideFiltersWhenIdle bool `yaml:"hideFiltersWhenIdle" mapstructure:"hideFiltersWhenIdle"`
ColorTitlebar bool `yaml:"colorTitlebar" mapstructure:"colorTitlebar"`
Theme string `yaml:"theme" mapstructure:"theme"`
MinimalGroupWidth int `yaml:"minimalGroupWidth" mapstructure:"minimalGroupWidth"`
AlertsPerGroup int `yaml:"alertsPerGroup" mapstructure:"alertsPerGroup"`
CollapseGroups string `yaml:"collapseGroups" mapstructure:"collapseGroups"`
HideFiltersWhenIdle bool `yaml:"hideFiltersWhenIdle" koanf:"hideFiltersWhenIdle"`
ColorTitlebar bool `yaml:"colorTitlebar" koanf:"colorTitlebar"`
Theme string `yaml:"theme" koanf:"theme"`
MinimalGroupWidth int `yaml:"minimalGroupWidth" koanf:"minimalGroupWidth"`
AlertsPerGroup int `yaml:"alertsPerGroup" koanf:"alertsPerGroup"`
CollapseGroups string `yaml:"collapseGroups" koanf:"collapseGroups"`
}
}

View File

@@ -8,7 +8,7 @@ for d in $(go list ./... | grep -vE 'prymitive/karma/internal/mapper/v017/(clien
go test \
-coverprofile=profile.out \
-coverpkg=$(go list ./... | grep -vE 'prymitive/karma/internal/mapper/v017/(client|models)' | tr '\n' ',') \
$d 2>&1 | grep -v 'warning: no packages being tested depend on matches for pattern'
$d 2>&1 | grep -v 'warning: no packages being tested depend on matches for pattern' | sed s/'of statements in .*'/''/g
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out