mirror of
https://github.com/prymitive/karma
synced 2026-02-13 20:59:53 +00:00
chore(backend): switch to github.com/knadh/koanf for config handling
This commit is contained in:
14
cmd/karma/testdata/default_config_file.txt
vendored
Normal file
14
cmd/karma/testdata/default_config_file.txt
vendored
Normal 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
|
||||
@@ -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:
|
||||
|
||||
6
cmd/karma/testdata/log_full_config_file.txt
vendored
6
cmd/karma/testdata/log_full_config_file.txt
vendored
@@ -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:"
|
||||
|
||||
@@ -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 ''"
|
||||
|
||||
4
cmd/karma/testdata/missing_config_file.txt
vendored
Normal file
4
cmd/karma/testdata/missing_config_file.txt
vendored
Normal 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'
|
||||
15
cmd/karma/testdata/silenceform_populatefromheader_invalid_regex.txt
vendored
Normal file
15
cmd/karma/testdata/silenceform_populatefromheader_invalid_regex.txt
vendored
Normal 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: ".++++"
|
||||
14
cmd/karma/testdata/silenceform_populatefromheader_missing_header.txt
vendored
Normal file
14
cmd/karma/testdata/silenceform_populatefromheader_missing_header.txt
vendored
Normal 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: "^(.+)$"
|
||||
14
cmd/karma/testdata/silenceform_populatefromheader_missing_regex.txt
vendored
Normal file
14
cmd/karma/testdata/silenceform_populatefromheader_missing_regex.txt
vendored
Normal 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
4
go.mod
@@ -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
8
go.sum
@@ -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=
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()")
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user