diff --git a/cmd/karma/main.go b/cmd/karma/main.go index 9c432c3ca..f594b399b 100644 --- a/cmd/karma/main.go +++ b/cmd/karma/main.go @@ -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) } diff --git a/cmd/karma/script_test.go b/cmd/karma/script_test.go index 7918eacb4..e9a4e71d8 100644 --- a/cmd/karma/script_test.go +++ b/cmd/karma/script_test.go @@ -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, })) } diff --git a/cmd/karma/testdata/invalid_config_alertmanager_timeout.txt b/cmd/karma/testdata/invalid_config_alertmanager_timeout.txt new file mode 100644 index 000000000..601aaa2be --- /dev/null +++ b/cmd/karma/testdata/invalid_config_alertmanager_timeout.txt @@ -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 diff --git a/cmd/karma/testdata/invalid_flag_alertmanager_timeout.txt b/cmd/karma/testdata/invalid_flag_alertmanager_timeout.txt new file mode 100644 index 000000000..74189ee7f --- /dev/null +++ b/cmd/karma/testdata/invalid_flag_alertmanager_timeout.txt @@ -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"' diff --git a/cmd/karma/testdata/invalid_ttl.txt b/cmd/karma/testdata/invalid_ttl.txt index ccdfb8ca7..20d3e4163 100644 --- a/cmd/karma/testdata/invalid_ttl.txt +++ b/cmd/karma/testdata/invalid_ttl.txt @@ -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''"' diff --git a/cmd/karma/testdata/log_full_config_file_invalid_values.txt b/cmd/karma/testdata/log_full_config_file_invalid_values.txt new file mode 100644 index 000000000..be70c984d --- /dev/null +++ b/cmd/karma/testdata/log_full_config_file_invalid_values.txt @@ -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'" diff --git a/cmd/karma/views_test.go b/cmd/karma/views_test.go index 5a64d5cdc..4d64c05f7 100644 --- a/cmd/karma/views_test.go +++ b/cmd/karma/views_test.go @@ -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() diff --git a/internal/alertmanager/benchmark_test.go b/internal/alertmanager/benchmark_test.go index 9b9567cd4..fc0557783 100644 --- a/internal/alertmanager/benchmark_test.go +++ b/internal/alertmanager/benchmark_test.go @@ -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) } diff --git a/internal/alertmanager/dedup_test.go b/internal/alertmanager/dedup_test.go index 91145d49d..5640affff 100644 --- a/internal/alertmanager/dedup_test.go +++ b/internal/alertmanager/dedup_test.go @@ -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) } diff --git a/internal/config/config.go b/internal/config/config.go index ed6ab5fa1..bf6dd8169 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bd93dffe2..762fc0aa5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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()")