diff --git a/.drone.yml b/.drone.yml index aed188e..4dcd36e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ name: test steps: - name: test - image: golang:1.20 + image: golang:1.21 environment: VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} commands: @@ -30,7 +30,7 @@ trigger: steps: - name: build all binaries - image: golang:1.20 + image: golang:1.21 environment: VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} commands: diff --git a/Dockerfile.multi-stage b/Dockerfile.multi-stage index 6de5531..2ef1f43 100644 --- a/Dockerfile.multi-stage +++ b/Dockerfile.multi-stage @@ -1,4 +1,4 @@ -FROM golang:1.20 AS builder +FROM golang:1.21 AS builder WORKDIR /app diff --git a/config.go b/config.go index 721a5e8..0b6b22b 100644 --- a/config.go +++ b/config.go @@ -6,21 +6,26 @@ import ( "time" "git.iamthefij.com/iamthefij/slog" - /* - * "github.com/hashicorp/hcl/v2" - * "github.com/hashicorp/hcl/v2/gohcl" - */ "github.com/hashicorp/hcl/v2/hclsimple" ) -var errInvalidConfig = errors.New("Invalid configuration") +var ( + ErrLoadingConfig = errors.New("Failed to load or parse configuration") + ErrConfigInit = errors.New("Failed to initialize configuration") + ErrInvalidConfig = errors.New("Invalid configuration") + ErrNoAlerts = errors.New("No alerts provided") + ErrInvalidAlert = errors.New("Invalid alert configuration") + ErrNoMonitors = errors.New("No monitors provided") + ErrInvalidMonitor = errors.New("Invalid monitor configuration") + ErrUnknownAlert = errors.New("Unknown alert") +) // Config type is contains all provided user configuration type Config struct { CheckIntervalStr string `hcl:"check_interval"` CheckInterval time.Duration - DefaultAlertAfter *int `hcl:"default_alert_after,optional"` + DefaultAlertAfter int `hcl:"default_alert_after,optional"` DefaultAlertEvery *int `hcl:"default_alert_every,optional"` DefaultAlertDown []string `hcl:"default_alert_down,optional"` DefaultAlertUp []string `hcl:"default_alert_up,optional"` @@ -30,6 +35,76 @@ type Config struct { alertLookup map[string]*Alert } +// Init performs extra initialization on top of loading the config from file +func (config *Config) Init() (err error) { + config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr) + if err != nil { + return fmt.Errorf("failed to parse top level check_interval duration: %w", err) + } + + if config.DefaultAlertAfter == 0 { + minAlertAfter := 1 + config.DefaultAlertAfter = minAlertAfter + } + + for _, monitor := range config.Monitors { + if err = monitor.Init( + config.DefaultAlertAfter, + config.DefaultAlertEvery, + config.DefaultAlertDown, + config.DefaultAlertUp, + ); err != nil { + return + } + } + + err = config.BuildAllTemplates() + + return +} + +// IsValid checks config validity and returns true if valid +func (config Config) IsValid() error { + var err error + + // Validate alerts + if len(config.Alerts) == 0 { + err = errors.Join(err, ErrNoAlerts) + } + + for _, alert := range config.Alerts { + if !alert.IsValid() { + err = errors.Join(err, fmt.Errorf("%w: %s", ErrInvalidAlert, alert.Name)) + } + } + + // Validate monitors + if len(config.Monitors) == 0 { + err = errors.Join(err, ErrNoMonitors) + } + + for _, monitor := range config.Monitors { + if !monitor.IsValid() { + err = errors.Join(err, fmt.Errorf("%w: %s", ErrInvalidMonitor, monitor.Name)) + } + + // Check that all Monitor alerts actually exist + for _, isUp := range []bool{true, false} { + for _, alertName := range monitor.GetAlertNames(isUp) { + if _, ok := config.GetAlert(alertName); !ok { + err = errors.Join( + err, + fmt.Errorf("%w: %s. %w: %s", ErrInvalidMonitor, monitor.Name, ErrUnknownAlert, alertName), + ) + } + } + } + } + + return err +} + +// GetAlert returns an alert by name func (c Config) GetAlert(name string) (*Alert, bool) { if c.alertLookup == nil { c.alertLookup = map[string]*Alert{} @@ -54,119 +129,25 @@ func (c *Config) BuildAllTemplates() (err error) { return } -// IsValid checks config validity and returns true if valid -func (config Config) IsValid() (isValid bool) { - isValid = true - - // Validate alerts - if len(config.Alerts) == 0 { - // This should never happen because there is a default alert named 'log' for now - slog.Errorf("Invalid alert configuration: Must provide at least one alert") - - isValid = false - } - - for _, alert := range config.Alerts { - if !alert.IsValid() { - slog.Errorf("Invalid alert configuration: %+v", alert.Name) - - isValid = false - } - } - - // Validate monitors - if len(config.Monitors) == 0 { - slog.Errorf("Invalid monitor configuration: Must provide at least one monitor") - - isValid = false - } - - for _, monitor := range config.Monitors { - if !monitor.IsValid() { - slog.Errorf("Invalid monitor configuration: %s", monitor.Name) - - isValid = false - } - // Check that all Monitor alerts actually exist - for _, isUp := range []bool{true, false} { - for _, alertName := range monitor.GetAlertNames(isUp) { - if _, ok := config.GetAlert(alertName); !ok { - slog.Errorf( - "Invalid monitor configuration: %s. Unknown alert %s", - monitor.Name, alertName, - ) - - isValid = false - } - } - } - } - - return isValid -} - -// Init performs extra initialization on top of loading the config from file -func (config *Config) Init() (err error) { - config.CheckInterval, err = time.ParseDuration(config.CheckIntervalStr) - if err != nil { - return fmt.Errorf("failed to parse top level check_interval duration: %w", err) - } - - for _, monitor := range config.Monitors { - // TODO: Move this to a Monitor.Init() method - - // Parse the check_interval string into a time.Duration - if monitor.CheckIntervalStr != nil { - monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr) - if err != nil { - return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err) - } - } - - // Set default values for monitor alerts - if monitor.AlertAfter == 0 && config.DefaultAlertAfter != nil { - monitor.AlertAfter = *config.DefaultAlertAfter - } else if monitor.AlertAfter == 0 { - monitor.AlertAfter = 1 - } - - if monitor.AlertEvery == nil { - monitor.AlertEvery = config.DefaultAlertEvery - } - - if monitor.AlertDown == nil { - monitor.AlertDown = config.DefaultAlertDown - } - - if monitor.AlertUp == nil { - monitor.AlertUp = config.DefaultAlertUp - } - } - - err = config.BuildAllTemplates() - - return -} - // LoadConfig will read config from the given path and parse it -func LoadConfig(filePath string) (config Config, err error) { - err = hclsimple.DecodeFile(filePath, nil, &config) - if err != nil { - return +func LoadConfig(filePath string) (Config, error) { + var config Config + + if err := hclsimple.DecodeFile(filePath, nil, &config); err != nil { + slog.Debugf("Failed to load config from %s: %v", filePath, err) + return config, errors.Join(ErrLoadingConfig, err) } slog.Debugf("Config values:\n%v\n", config) // Finish initializing configuration - if err = config.Init(); err != nil { - return + if err := config.Init(); err != nil { + return config, errors.Join(ErrConfigInit, err) } - if !config.IsValid() { - err = errInvalidConfig - - return + if err := config.IsValid(); err != nil { + return config, errors.Join(ErrInvalidConfig, err) } - return config, err + return config, nil } diff --git a/config_test.go b/config_test.go index dc72bad..6a6d3d4 100644 --- a/config_test.go +++ b/config_test.go @@ -1,6 +1,7 @@ package main_test import ( + "errors" "testing" m "git.iamthefij.com/iamthefij/minitor-go" @@ -8,16 +9,18 @@ import ( func TestLoadConfig(t *testing.T) { cases := []struct { - configPath string - expectErr bool - name string + configPath string + expectedErr error + name string }{ - {"./test/does-not-exist", true, "Invalid config path"}, - {"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"}, - {"./test/invalid-config-type.hcl", true, "Invalid config type for key"}, - {"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"}, - {"./test/valid-config-default-values.hcl", false, "Valid config file with default values"}, - {"./test/valid-config.hcl", false, "Valid config file"}, + {"./test/does-not-exist", m.ErrLoadingConfig, "Invalid config path"}, + {"./test/invalid-config-wrong-hcl-type.hcl", m.ErrLoadingConfig, "Incorrect HCL type"}, + {"./test/invalid-config-missing-alerts.hcl", m.ErrNoAlerts, "Invalid config missing alerts"}, + {"./test/invalid-config-missing-alerts.hcl", m.ErrInvalidConfig, "Invalid config general"}, + {"./test/invalid-config-invalid-duration.hcl", m.ErrConfigInit, "Invalid config type for key"}, + {"./test/invalid-config-unknown-alert.hcl", m.ErrUnknownAlert, "Invalid config unknown alert"}, + {"./test/valid-config-default-values.hcl", nil, "Valid config file with default values"}, + {"./test/valid-config.hcl", nil, "Valid config file"}, } for _, c := range cases { c := c @@ -27,9 +30,10 @@ func TestLoadConfig(t *testing.T) { _, err := m.LoadConfig(c.configPath) hasErr := (err != nil) + expectErr := (c.expectedErr != nil) - if hasErr != c.expectErr { - t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectErr, err) + if hasErr != expectErr || !errors.Is(err, c.expectedErr) { + t.Errorf("LoadConfig(%v), expected_error=%v actual=%v", c.name, c.expectedErr, err) } }) } diff --git a/go.mod b/go.mod index 605578e..d7bc1e6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.iamthefij.com/iamthefij/minitor-go -go 1.20 +go 1.21 require ( git.iamthefij.com/iamthefij/slog v1.3.0 diff --git a/go.sum b/go.sum index 579b8c1..fd3e92a 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,7 @@ github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJb github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -41,6 +42,7 @@ github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5E github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= diff --git a/monitor.go b/monitor.go index c7c83b6..fef7472 100644 --- a/monitor.go +++ b/monitor.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "math" "os/exec" "time" @@ -31,8 +32,40 @@ type Monitor struct { //nolint:maligned lastCheckDuration time.Duration } -// IsValid returns a boolean indicating if the Monitor has been correctly -// configured +// Init initializes the Monitor with default values +func (monitor *Monitor) Init(defaultAlertAfter int, defaultAlertEvery *int, defaultAlertDown []string, defaultAlertUp []string) error { + // Parse the check_interval string into a time.Duration + if monitor.CheckIntervalStr != nil { + var err error + + monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr) + if err != nil { + return fmt.Errorf("failed to parse check_interval duration for monitor %s: %w", monitor.Name, err) + } + } + + // Set default values for monitor alerts + if monitor.AlertAfter == 0 { + minAlertAfter := 1 + monitor.AlertAfter = max(defaultAlertAfter, minAlertAfter) + } + + if monitor.AlertEvery == nil { + monitor.AlertEvery = defaultAlertEvery + } + + if len(monitor.AlertDown) == 0 { + monitor.AlertDown = defaultAlertDown + } + + if len(monitor.AlertUp) == 0 { + monitor.AlertUp = defaultAlertUp + } + + return nil +} + +// IsValid returns a boolean indicating if the Monitor has been correctly configured func (monitor Monitor) IsValid() bool { // TODO: Refactor and return an error containing more information on what was invalid hasCommand := len(monitor.Command) > 0 @@ -53,8 +86,7 @@ func (monitor Monitor) LastOutput() string { return monitor.lastOutput } -// ShouldCheck returns a boolean indicating if the Monitor is ready to be -// be checked again +// ShouldCheck returns a boolean indicating if the Monitor is ready to be be checked again func (monitor Monitor) ShouldCheck() bool { if monitor.lastCheck.IsZero() || monitor.CheckInterval == 0 { return true @@ -65,8 +97,7 @@ func (monitor Monitor) ShouldCheck() bool { return sinceLastCheck >= monitor.CheckInterval } -// Check will run the command configured by the Monitor and return a status -// and a possible AlertNotice +// Check will run the command configured by the Monitor and return a status and a possible AlertNotice func (monitor *Monitor) Check() (bool, *AlertNotice) { var cmd *exec.Cmd if len(monitor.Command) > 0 { @@ -105,6 +136,15 @@ func (monitor *Monitor) Check() (bool, *AlertNotice) { return isSuccess, alertNotice } +// GetAlertNames gives a list of alert names for a given monitor status +func (monitor Monitor) GetAlertNames(up bool) []string { + if up { + return monitor.AlertUp + } + + return monitor.AlertDown +} + // IsUp returns the status of the current monitor func (monitor Monitor) IsUp() bool { return monitor.alertCount == 0 @@ -173,15 +213,6 @@ func (monitor *Monitor) failure() (notice *AlertNotice) { return notice } -// GetAlertNames gives a list of alert names for a given monitor status -func (monitor Monitor) GetAlertNames(up bool) []string { - if up { - return monitor.AlertUp - } - - return monitor.AlertDown -} - func (monitor Monitor) createAlertNotice(isUp bool) *AlertNotice { // TODO: Maybe add something about recovery status here return &AlertNotice{ diff --git a/test/invalid-config-type.hcl b/test/invalid-config-invalid-duration.hcl similarity index 100% rename from test/invalid-config-type.hcl rename to test/invalid-config-invalid-duration.hcl diff --git a/test/invalid-config-wrong-hcl-type.hcl b/test/invalid-config-wrong-hcl-type.hcl new file mode 100644 index 0000000..6234af9 --- /dev/null +++ b/test/invalid-config-wrong-hcl-type.hcl @@ -0,0 +1,12 @@ +check_interval = "1s" + +alert "log_command" { + command = "should be a list" +} + +monitor "Command" { + command = ["echo", "$PATH"] + alert_down = ["log_command"] + alert_every = 2 + check_interval = "10s" +}