Merge pull request #1406 from prymitive/refactor-flags

chore(backend): refactor config flag setup for better test coverage
This commit is contained in:
Łukasz Mierzwa
2020-02-05 17:18:11 +00:00
committed by GitHub
11 changed files with 205 additions and 80 deletions

View File

@@ -190,18 +190,24 @@ func setupLogger() error {
return nil
}
func mainSetup() (*gin.Engine, error) {
printVersion := pflag.Bool("version", false, "Print version and exit")
validateConfig := pflag.Bool("check-config", false, "Validate configuration and exit")
pflag.Parse()
func mainSetup(errorHandling pflag.ErrorHandling) (*gin.Engine, error) {
f := pflag.NewFlagSet("karma", errorHandling)
printVersion := f.Bool("version", false, "Print version and exit")
validateConfig := f.Bool("check-config", false, "Validate configuration and exit")
config.SetupFlags(f)
err := f.Parse(os.Args[1:])
if err != nil {
return nil, err
}
if *printVersion {
fmt.Println(version)
return nil, nil
}
configFile := config.Config.Read()
err := setupLogger()
configFile := config.Config.Read(f)
err = setupLogger()
if err != nil {
return nil, err
}
@@ -212,7 +218,7 @@ func mainSetup() (*gin.Engine, error) {
// timer duration cannot be zero second or a negative one
if config.Config.Alertmanager.Interval <= time.Second*0 {
return nil, fmt.Errorf("Invalid AlertmanagerTTL value '%v'", config.Config.Alertmanager.Interval)
return nil, fmt.Errorf("Invalid alertmanager.interval value '%v'", config.Config.Alertmanager.Interval)
}
log.Infof("Version: %s", version)
@@ -285,7 +291,7 @@ func mainSetup() (*gin.Engine, error) {
}
func main() {
router, err := mainSetup()
router, err := mainSetup(pflag.ExitOnError)
if err != nil {
log.Fatal(err)
}

View File

@@ -5,14 +5,21 @@ import (
"testing"
"github.com/rogpeppe/go-internal/testscript"
"github.com/spf13/pflag"
log "github.com/sirupsen/logrus"
)
func mainShoulFail() int {
_, err := mainSetup()
defer func() { log.StandardLogger().ExitFunc = nil }()
var wasFatal bool
log.StandardLogger().ExitFunc = func(int) { wasFatal = true }
_, err := mainSetup(pflag.ContinueOnError)
if err != nil {
log.Error(err)
} else if wasFatal {
return 100
} else {
log.Error("No error logged")
return 100
@@ -20,8 +27,15 @@ func mainShoulFail() int {
return 0
}
func mainShoulFailNoTimestamp() int {
log.SetFormatter(&log.TextFormatter{
DisableTimestamp: true,
})
return mainShoulFail()
}
func mainShouldWork() int {
_, err := mainSetup()
_, err := mainSetup(pflag.ContinueOnError)
if err != nil {
log.Error(err)
return 100
@@ -31,8 +45,9 @@ func mainShouldWork() int {
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"karma.bin-should-fail": mainShoulFail,
"karma.bin-should-work": mainShouldWork,
"karma.bin-should-fail": mainShoulFail,
"karma.bin-should-fail-no-timestamp": mainShoulFailNoTimestamp,
"karma.bin-should-work": mainShouldWork,
}))
}

View File

@@ -0,0 +1,11 @@
# 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"'
-- karma.yaml --
alertmanager:
interval: abc123
servers:
- name: am
uri: https://localhost:9093

View File

@@ -0,0 +1,4 @@
# Raises an error if we pass alertmanager.timeout value that doesn't parse
karma.bin-should-fail --log.format=text --log.config=false --log.level=error --alertmanager.timeout=abc123 --alertmanager.uri=http://localhost
! stdout .
stderr 'level=error msg="invalid argument \\"abc123\\" for \\"--alertmanager.timeout\\" flag: time: invalid duration abc123"'

View File

@@ -1,4 +1,4 @@
# Raises an error if negative refresh interval is passed
karma.bin-should-fail --log.format=text --log.config=false --log.level=error --alertmanager.interval=-4s
! stdout .
stderr 'msg="Invalid AlertmanagerTTL value ''-4s''"'
stderr 'msg="Invalid alertmanager.interval value ''-4s''"'

View File

@@ -0,0 +1,64 @@
# Print out and compare logged config set via config file that includes invalid values
karma.bin-should-fail-no-timestamp --config.file=karma.yaml --check-config
! stdout .
cmp stderr expected.stderr
-- karma.yaml --
alertmanager:
interval: jjs88
servers:
- name: ha1
uri: "http://localhost:9093"
timeout: bbb
proxy: YEs
- name: ha2
uri: "http://localhost:9094"
timeout: 11
readonly: 1
- name: local
uri: http://localhost:9095
timeout: z
proxy: true
readonly: 0
headers:
- X-Auth-Test=some-token-or-other-string
- name: client-auth
uri: https://localhost:9096
timeout: 10s
tls:
ca: ca.pem
cert: cert.pem
key: key.pem
alertAcknowledgement:
enabled: zzz
duration: 7m0s
author: karma
commentPrefix: ACK!
annotations:
default:
hidden: z
hidden: {}
visible:
- visible
filters:
default: []
karma:
name: karma-demo
log:
level: 123
format: foo
ui:
refresh: 10sm
hideFiltersWhenIdle: z
colorTitlebar: yum
theme: x
minimalGroupWidth: abc4
alertsPerGroup: 5a
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'"

View File

@@ -15,6 +15,7 @@ import (
"github.com/prymitive/karma/internal/mock"
"github.com/prymitive/karma/internal/models"
"github.com/prymitive/karma/internal/slices"
"github.com/spf13/pflag"
cache "github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
@@ -30,7 +31,11 @@ func mockConfig() {
log.SetLevel(log.ErrorLevel)
os.Setenv("ALERTMANAGER_URI", "http://localhost")
os.Setenv("LABELS_COLOR_UNIQUE", "alertname")
config.Config.Read()
f := pflag.NewFlagSet(".", pflag.ExitOnError)
config.SetupFlags(f)
config.Config.Read(f)
if !upstreamSetup {
upstreamSetup = true
err := setupUpstreams()

View File

@@ -6,6 +6,7 @@ import (
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/config"
"github.com/spf13/pflag"
)
func BenchmarkDedupAlerts(b *testing.B) {
@@ -29,7 +30,11 @@ func BenchmarkDedupAutocomplete(b *testing.B) {
func BenchmarkDedupColors(b *testing.B) {
os.Setenv("LABELS_COLOR_UNIQUE", "cluster instance @receiver")
os.Setenv("ALERTMANAGER_URI", "http://localhost")
config.Config.Read()
f := pflag.NewFlagSet(".", pflag.ExitOnError)
config.SetupFlags(f)
config.Config.Read(f)
if err := pullAlerts(); err != nil {
b.Error(err)
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/jarcoal/httpmock"
"github.com/spf13/pflag"
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/config"
@@ -50,6 +51,12 @@ func pullAlerts() error {
return nil
}
func mockConfigRead() {
f := pflag.NewFlagSet(".", pflag.ExitOnError)
config.SetupFlags(f)
config.Config.Read(f)
}
func TestDedupAlerts(t *testing.T) {
if err := pullAlerts(); err != nil {
t.Error(err)
@@ -92,7 +99,7 @@ func TestDedupAlertsWithoutLabels(t *testing.T) {
func TestDedupSilences(t *testing.T) {
os.Setenv("ALERTMANAGER_URI", "http://localhost")
config.Config.Read()
mockConfigRead()
if err := pullAlerts(); err != nil {
t.Error(err)
}
@@ -123,7 +130,7 @@ func TestDedupAutocomplete(t *testing.T) {
func TestDedupColors(t *testing.T) {
os.Setenv("LABELS_COLOR_UNIQUE", "cluster instance @receiver")
os.Setenv("ALERTMANAGER_URI", "http://localhost")
config.Config.Read()
mockConfigRead()
if err := pullAlerts(); err != nil {
t.Error(err)
}
@@ -136,7 +143,7 @@ func TestDedupColors(t *testing.T) {
func TestDedupKnownLabels(t *testing.T) {
os.Setenv("ALERTMANAGER_URI", "http://localhost")
config.Config.Read()
mockConfigRead()
if err := pullAlerts(); err != nil {
t.Error(err)
}
@@ -149,7 +156,7 @@ func TestDedupKnownLabels(t *testing.T) {
func TestDedupKnownLabelValues(t *testing.T) {
os.Setenv("ALERTMANAGER_URI", "http://localhost")
config.Config.Read()
mockConfigRead()
if err := pullAlerts(); err != nil {
t.Error(err)
}
@@ -163,7 +170,7 @@ func TestDedupKnownLabelValues(t *testing.T) {
func TestStripReceivers(t *testing.T) {
os.Setenv("RECEIVERS_STRIP", "by-name by-cluster-service")
os.Setenv("ALERTMANAGER_URI", "http://localhost")
config.Config.Read()
mockConfigRead()
if err := pullAlerts(); err != nil {
t.Error(err)
}

View File

@@ -23,99 +23,100 @@ var (
Config configSchema
)
func init() {
pflag.Duration("alertmanager.interval", time.Minute,
// SetupFlags is used to attach configuration flags to the main flag set
func SetupFlags(f *pflag.FlagSet) {
f.Duration("alertmanager.interval", time.Minute,
"Interval for fetching data from Alertmanager servers")
pflag.String("alertmanager.name", "default",
f.String("alertmanager.name", "default",
"Name for the Alertmanager server (only used with simplified config)")
pflag.String("alertmanager.uri", "",
f.String("alertmanager.uri", "",
"Alertmanager server URI (only used with simplified config)")
pflag.String("alertmanager.external_uri", "",
f.String("alertmanager.external_uri", "",
"Alertmanager server URI used for web UI links (only used with simplified config)")
pflag.Duration("alertmanager.timeout", time.Second*40,
f.Duration("alertmanager.timeout", time.Second*40,
"Timeout for requests sent to the Alertmanager server (only used with simplified config)")
pflag.Bool("alertmanager.proxy", false,
f.Bool("alertmanager.proxy", false,
"Proxy all client requests to Alertmanager via karma (only used with simplified config)")
pflag.Bool("alertmanager.readonly", false,
f.Bool("alertmanager.readonly", false,
"Enable read-only mode that disable silence management (only used with simplified config)")
pflag.String("karma.name", "karma", "Name for the karma instance")
f.String("karma.name", "karma", "Name for the karma instance")
pflag.Bool("alertAcknowledgement.enabled", false, "Enable alert acknowledging")
pflag.Duration("alertAcknowledgement.duration", time.Minute*15, "Initial silence duration when acknowledging alerts with short lived silences")
pflag.String("alertAcknowledgement.author", "karma", "Default silence author when acknowledging alerts with short lived silences")
pflag.String("alertAcknowledgement.commentPrefix", "ACK!", "Comment prefix used when acknowledging alerts with short lived silences")
f.Bool("alertAcknowledgement.enabled", false, "Enable alert acknowledging")
f.Duration("alertAcknowledgement.duration", time.Minute*15, "Initial silence duration when acknowledging alerts with short lived silences")
f.String("alertAcknowledgement.author", "karma", "Default silence author when acknowledging alerts with short lived silences")
f.String("alertAcknowledgement.commentPrefix", "ACK!", "Comment prefix used when acknowledging alerts with short lived silences")
pflag.Bool(
f.Bool(
"annotations.default.hidden", false,
"Hide all annotations by default unless explicitly listed in the 'visible' list")
pflag.StringSlice("annotations.hidden", []string{},
f.StringSlice("annotations.hidden", []string{},
"List of annotations that are hidden by default")
pflag.StringSlice("annotations.visible", []string{},
f.StringSlice("annotations.visible", []string{},
"List of annotations that are visible by default")
pflag.StringSlice("annotations.keep", []string{},
f.StringSlice("annotations.keep", []string{},
"List of annotations to keep, all other annotations will be stripped")
pflag.StringSlice("annotations.strip", []string{}, "List of annotations to ignore")
f.StringSlice("annotations.strip", []string{}, "List of annotations to ignore")
pflag.String("config.file", "", "Full path to the configuration file")
f.String("config.file", "", "Full path to the configuration file")
pflag.String("custom.css", "", "Path to a file with custom CSS to load")
pflag.String("custom.js", "", "Path to a file with custom JavaScript to load")
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")
pflag.Bool("debug", false, "Enable debug mode")
f.Bool("debug", false, "Enable debug mode")
pflag.StringSlice("filters.default", []string{}, "List of default filters")
f.StringSlice("filters.default", []string{}, "List of default filters")
pflag.StringSlice("labels.color.static", []string{},
f.StringSlice("labels.color.static", []string{},
"List of label names that should have the same (but distinct) color")
pflag.StringSlice("labels.color.unique", []string{},
f.StringSlice("labels.color.unique", []string{},
"List of label names that should have unique color")
pflag.StringSlice("labels.keep", []string{},
f.StringSlice("labels.keep", []string{},
"List of labels to keep, all other labels will be stripped")
pflag.StringSlice("labels.strip", []string{}, "List of labels to ignore")
f.StringSlice("labels.strip", []string{}, "List of labels to ignore")
pflag.String("grid.sorting.order", "startsAt", "Default sort order for alert grid")
pflag.Bool("grid.sorting.reverse", true, "Reverse sort order")
pflag.String("grid.sorting.label", "alertname", "Label name to use when sorting alert grid by label")
f.String("grid.sorting.order", "startsAt", "Default sort order for alert grid")
f.Bool("grid.sorting.reverse", true, "Reverse sort order")
f.String("grid.sorting.label", "alertname", "Label name to use when sorting alert grid by label")
pflag.Bool("log.config", true, "Log used configuration to log on startup")
pflag.String("log.level", "info",
f.Bool("log.config", true, "Log used configuration to log on startup")
f.String("log.level", "info",
"Log level, one of: debug, info, warning, error, fatal and panic")
pflag.String("log.format", "text",
f.String("log.format", "text",
"Log format, one of: text, json")
pflag.Bool("log.timestamp", true, "Add timestamps to all log messages")
f.Bool("log.timestamp", true, "Add timestamps to all log messages")
pflag.StringSlice("receivers.keep", []string{},
f.StringSlice("receivers.keep", []string{},
"List of receivers to keep, all alerts with different receivers will be ignored")
pflag.StringSlice("receivers.strip", []string{},
f.StringSlice("receivers.strip", []string{},
"List of receivers to not display alerts for")
pflag.StringSlice("silenceform.strip.labels", []string{}, "List of labels to ignore when auto-filling silence form from alerts")
pflag.String("silenceform.author.populate_from_header.header", "", "Header to read the default silence author from")
pflag.String("silenceform.author.populate_from_header.value_re", "", "Header value regex to read the default silence author")
f.StringSlice("silenceform.strip.labels", []string{}, "List of labels to ignore when auto-filling silence form from alerts")
f.String("silenceform.author.populate_from_header.header", "", "Header to read the default silence author from")
f.String("silenceform.author.populate_from_header.value_re", "", "Header value regex to read the default silence author")
pflag.String("listen.address", "", "IP/Hostname to listen on")
pflag.Int("listen.port", 8080, "HTTP port to listen on")
pflag.String("listen.prefix", "/", "URL prefix")
f.String("listen.address", "", "IP/Hostname to listen on")
f.Int("listen.port", 8080, "HTTP port to listen on")
f.String("listen.prefix", "/", "URL prefix")
pflag.String("sentry.public", "", "Sentry DSN for Go exceptions")
pflag.String("sentry.private", "", "Sentry DSN for JavaScript exceptions")
f.String("sentry.public", "", "Sentry DSN for Go exceptions")
f.String("sentry.private", "", "Sentry DSN for JavaScript exceptions")
pflag.Duration("ui.refresh", time.Second*30, "UI refresh interval")
pflag.Bool("ui.hideFiltersWhenIdle", true, "Hide the filters bar when idle")
pflag.Bool("ui.colorTitlebar", false, "Color alert group titlebar based on alert state")
pflag.String("ui.theme", "auto", "Default theme, 'light', 'dark' or 'auto' (follow browser preference)")
pflag.Int("ui.minimalGroupWidth", 420, "Minimal width for each alert group on the grid")
pflag.Int("ui.alertsPerGroup", 5, "Default number of alerts to show for each alert group")
pflag.String("ui.collapseGroups", "collapsedOnMobile", "Default state for alert groups")
f.Duration("ui.refresh", time.Second*30, "UI refresh interval")
f.Bool("ui.hideFiltersWhenIdle", true, "Hide the filters bar when idle")
f.Bool("ui.colorTitlebar", false, "Color alert group titlebar based on alert state")
f.String("ui.theme", "auto", "Default theme, 'light', 'dark' or 'auto' (follow browser preference)")
f.Int("ui.minimalGroupWidth", 420, "Minimal width for each alert group on the grid")
f.Int("ui.alertsPerGroup", 5, "Default number of alerts to show for each alert group")
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() string {
func (config *configSchema) Read(flags *pflag.FlagSet) string {
v := viper.New()
err := v.BindPFlags(pflag.CommandLine)
err := v.BindPFlags(flags)
if err != nil {
log.Errorf("Failed to bind flag set to the configuration: %s", err)
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/prymitive/karma/internal/uri"
"github.com/spf13/pflag"
"github.com/pmezard/go-difflib/difflib"
@@ -173,6 +174,12 @@ ui:
}
}
func mockConfigRead() {
f := pflag.NewFlagSet(".", pflag.ExitOnError)
SetupFlags(f)
Config.Read(f)
}
func TestReadConfig(t *testing.T) {
resetEnv()
log.SetLevel(log.ErrorLevel)
@@ -194,7 +201,7 @@ func TestReadConfig(t *testing.T) {
os.Setenv("LISTEN_PORT", "80")
os.Setenv("SENTRY_PRIVATE", "secret key")
os.Setenv("SENTRY_PUBLIC", "public key")
Config.Read()
mockConfigRead()
testReadConfig(t)
}
@@ -206,7 +213,7 @@ func TestReadSimpleConfig(t *testing.T) {
os.Setenv("ALERTMANAGER_TIMEOUT", "15s")
os.Setenv("ALERTMANAGER_PROXY", "true")
os.Setenv("ALERTMANAGER_INTERVAL", "3m")
Config.Read()
mockConfigRead()
if len(Config.Alertmanager.Servers) != 1 {
t.Errorf("Expected 1 Alertmanager server, got %d", len(Config.Alertmanager.Servers))
} else {
@@ -262,7 +269,7 @@ func TestUrlSecretTest(t *testing.T) {
// FIXME check logged values
func TestLogValues(t *testing.T) {
Config.Read()
mockConfigRead()
Config.LogValues()
}
@@ -275,7 +282,7 @@ func TestInvalidSilenceFormRegex(t *testing.T) {
var wasFatal bool
log.StandardLogger().ExitFunc = func(int) { wasFatal = true }
Config.Read()
mockConfigRead()
if !wasFatal {
t.Error("Invalid silence form regex didn't cause log.Fatal()")
@@ -291,7 +298,7 @@ func TestInvalidGridSortingOrder(t *testing.T) {
var wasFatal bool
log.StandardLogger().ExitFunc = func(int) { wasFatal = true }
Config.Read()
mockConfigRead()
if !wasFatal {
t.Error("Invalid grid.sorting.order value didn't cause log.Fatal()")
@@ -307,7 +314,7 @@ func TestInvalidUICollapseGroups(t *testing.T) {
var wasFatal bool
log.StandardLogger().ExitFunc = func(int) { wasFatal = true }
Config.Read()
mockConfigRead()
if !wasFatal {
t.Error("Invalid ui.collapseGroups value didn't cause log.Fatal()")
@@ -323,7 +330,7 @@ func TestInvalidUITheme(t *testing.T) {
var wasFatal bool
log.StandardLogger().ExitFunc = func(int) { wasFatal = true }
Config.Read()
mockConfigRead()
if !wasFatal {
t.Error("Invalid ui.theme value didn't cause log.Fatal()")