diff --git a/cmd/sslcert/main.go b/cmd/sslcert/main.go index d942cc3..eb7b2f1 100644 --- a/cmd/sslcert/main.go +++ b/cmd/sslcert/main.go @@ -16,7 +16,7 @@ import ( "nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/plugin" - "nocternity.net/gomonop/pkg/program" + "nocternity.net/gomonop/pkg/results" ) //-------------------------------------------------------------------------------------------------------- @@ -167,7 +167,7 @@ type programFlags struct { // Program data including configuration and runtime data. type checkProgram struct { programFlags // Flags from the command line - plugin *plugin.Plugin // Plugin output state + plugin *results.Results // Plugin output state certificate *x509.Certificate // X.509 certificate from the server } @@ -207,37 +207,37 @@ func (flags *programFlags) parseArguments() { } // Initialise the monitoring check program. -func NewProgram() program.Program { +func NewProgram() plugin.Plugin { program := &checkProgram{ - plugin: plugin.New("Certificate check"), + plugin: results.New("Certificate check"), } program.parseArguments() return program } -// Terminate the monitoring check program. -func (program *checkProgram) Done() { - program.plugin.Done() +// Return the program's output value. +func (program *checkProgram) Results() *results.Results { + return program.plugin } // Check the values that were specified from the command line. Returns true // if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no hostname specified") + program.plugin.SetState(results.StatusUnknown, "no hostname specified") return false } if program.port < 1 || program.port > 65535 { - program.plugin.SetState(plugin.UNKNOWN, "invalid or missing port number") + program.plugin.SetState(results.StatusUnknown, "invalid or missing port number") return false } if program.warn != -1 && program.crit != -1 && program.warn <= program.crit { - program.plugin.SetState(plugin.UNKNOWN, "nonsensical thresholds") + program.plugin.SetState(results.StatusUnknown, "nonsensical thresholds") return false } if _, ok := certGetters[program.startTLS]; !ok { errstr := "unsupported StartTLS protocol " + program.startTLS - program.plugin.SetState(plugin.UNKNOWN, errstr) + program.plugin.SetState(results.StatusUnknown, errstr) return false } program.hostname = strings.ToLower(program.hostname) @@ -262,13 +262,13 @@ func (program *checkProgram) getCertificate() error { // matches the requested host name. func (program *checkProgram) checkSANlessCertificate() bool { if !program.ignoreCnOnly || len(program.extraNames) != 0 { - program.plugin.SetState(plugin.WARNING, + program.plugin.SetState(results.StatusWarning, "certificate doesn't have SAN domain names") return false } dn := strings.ToLower(program.certificate.Subject.String()) if !strings.HasPrefix(dn, fmt.Sprintf("cn=%s,", program.hostname)) { - program.plugin.SetState(plugin.CRITICAL, "incorrect certificate CN") + program.plugin.SetState(results.StatusCritical, "incorrect certificate CN") return false } return true @@ -298,7 +298,7 @@ func (program *checkProgram) checkNames() bool { certificateIsOk = program.checkHostName(name) && certificateIsOk } if !certificateIsOk { - program.plugin.SetState(plugin.CRITICAL, "names missing from SAN domain names") + program.plugin.SetState(results.StatusCritical, "names missing from SAN domain names") } return certificateIsOk } @@ -306,26 +306,26 @@ func (program *checkProgram) checkNames() bool { // Check a certificate's time to expiry against the warning and critical // thresholds, returning a status code and description based on these // values. -func (program *checkProgram) checkCertificateExpiry(tlDays int) (plugin.Status, string) { +func (program *checkProgram) checkCertificateExpiry(tlDays int) (results.Status, string) { if tlDays <= 0 { - return plugin.CRITICAL, "certificate expired" + return results.StatusCritical, "certificate expired" } var limitStr string - var state plugin.Status + var state results.Status switch { case program.crit > 0 && tlDays <= program.crit: limitStr = fmt.Sprintf(" (<= %d)", program.crit) - state = plugin.CRITICAL + state = results.StatusCritical case program.warn > 0 && tlDays <= program.warn: limitStr = fmt.Sprintf(" (<= %d)", program.warn) - state = plugin.WARNING + state = results.StatusWarning default: limitStr = "" - state = plugin.OK + state = results.StatusOK } statusString := fmt.Sprintf("certificate will expire in %d days%s", @@ -338,10 +338,10 @@ func (program *checkProgram) checkCertificateExpiry(tlDays int) (plugin.Status, func (program *checkProgram) setPerfData(tlDays int) { pdat := perfdata.New("validity", perfdata.UomNone, strconv.Itoa(tlDays)) if program.crit > 0 { - pdat.SetCrit(perfdata.PDRMax(strconv.Itoa(program.crit))) + pdat.SetCrit(perfdata.RangeMax(strconv.Itoa(program.crit))) } if program.warn > 0 { - pdat.SetWarn(perfdata.PDRMax(strconv.Itoa(program.warn))) + pdat.SetWarn(perfdata.RangeMax(strconv.Itoa(program.warn))) } program.plugin.AddPerfData(pdat) } @@ -351,7 +351,7 @@ func (program *checkProgram) setPerfData(tlDays int) { func (program *checkProgram) RunCheck() { err := program.getCertificate() if err != nil { - program.plugin.SetState(plugin.UNKNOWN, err.Error()) + program.plugin.SetState(results.StatusUnknown, err.Error()) } else if program.checkNames() { timeLeft := time.Until(program.certificate.NotAfter) tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour)) diff --git a/cmd/zoneserial/main.go b/cmd/zoneserial/main.go index f00547a..f5a1f25 100644 --- a/cmd/zoneserial/main.go +++ b/cmd/zoneserial/main.go @@ -14,7 +14,7 @@ import ( "nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/plugin" - "nocternity.net/gomonop/pkg/program" + "nocternity.net/gomonop/pkg/results" ) //------------------------------------------------------------------------------------------------------- @@ -55,8 +55,8 @@ type programFlags struct { // Program data including configuration and runtime data. type checkProgram struct { - programFlags // Flags from the command line - plugin *plugin.Plugin // Plugin output state + programFlags // Flags from the command line + plugin *results.Results // Plugin output state } // Parse command line arguments and store their values. If the -h flag is present, @@ -77,43 +77,39 @@ func (flags *programFlags) parseArguments() { } // Initialise the monitoring check program. -func NewProgram() program.Program { +func NewProgram() plugin.Plugin { program := &checkProgram{ - plugin: plugin.New("DNS zone serial match check"), + plugin: results.New("DNS zone serial match check"), } program.parseArguments() return program } -// Terminate the monitoring check program. -func (program *checkProgram) Done() { - if r := recover(); r != nil { - program.plugin.SetState(plugin.UNKNOWN, "Internal error") - program.plugin.AddLinef("Error info: %v", r) - } - program.plugin.Done() +// Return the program's output value. +func (program *checkProgram) Results() *results.Results { + return program.plugin } // Check the values that were specified from the command line. Returns true if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no DNS hostname specified") + program.plugin.SetState(results.StatusUnknown, "no DNS hostname specified") return false } if program.port < 1 || program.port > 65535 { - program.plugin.SetState(plugin.UNKNOWN, "invalid DNS port number") + program.plugin.SetState(results.StatusUnknown, "invalid DNS port number") return false } if program.zone == "" { - program.plugin.SetState(plugin.UNKNOWN, "no DNS zone specified") + program.plugin.SetState(results.StatusUnknown, "no DNS zone specified") return false } if program.rsHostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no reference DNS hostname specified") + program.plugin.SetState(results.StatusUnknown, "no reference DNS hostname specified") return false } if program.rsPort < 1 || program.rsPort > 65535 { - program.plugin.SetState(plugin.UNKNOWN, "invalid reference DNS port number") + program.plugin.SetState(results.StatusUnknown, "invalid reference DNS port number") return false } program.hostname = strings.ToLower(program.hostname) @@ -180,12 +176,12 @@ func (program *checkProgram) RunCheck() { cOk, cSerial := program.getSerial("checked", checkResponse) rOk, rSerial := program.getSerial("reference", refResponse) if !(cOk && rOk) { - program.plugin.SetState(plugin.UNKNOWN, "could not read serials") + program.plugin.SetState(results.StatusUnknown, "could not read serials") return } if cSerial == rSerial { - program.plugin.SetState(plugin.OK, "serials match") + program.plugin.SetState(results.StatusOK, "serials match") } else { - program.plugin.SetState(plugin.CRITICAL, "serials mismatch") + program.plugin.SetState(results.StatusCritical, "serials mismatch") } } diff --git a/go.mod b/go.mod index fbbf082..111d2ca 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,11 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 17c7bb2..37c20f7 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/karrick/golf v1.4.0 h1:9i9HnUh7uCyUFJhIqg311HBibw4f2pbGldi0ZM2FhaQ= github.com/karrick/golf v1.4.0/go.mod h1:qGN0IhcEL+IEgCXp00RvH32UP59vtwc8w5YcIdArNRk= github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA= github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -19,3 +25,6 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index e8ce79a..fd15700 100644 --- a/main.go +++ b/main.go @@ -7,33 +7,34 @@ import ( "nocternity.net/gomonop/cmd/sslcert" "nocternity.net/gomonop/cmd/zoneserial" - "nocternity.net/gomonop/pkg/program" + "nocternity.net/gomonop/pkg/plugin" + "nocternity.net/gomonop/pkg/results" "nocternity.net/gomonop/pkg/version" ) var ( - programs map[string]program.Builder = map[string]program.Builder{ + plugins map[string]plugin.Builder = map[string]plugin.Builder{ "check_ssl_certificate": sslcert.NewProgram, "check_zone_serial": zoneserial.NewProgram, } ) -func getProgram() program.Program { +func getPlugin() plugin.Plugin { ownName := filepath.Base(os.Args[0]) - if builder, ok := programs[ownName]; ok { + if builder, ok := plugins[ownName]; ok { return builder() } if len(os.Args) < 2 { - fmt.Printf("Syntax: %s [arguments]\n", ownName) - fmt.Printf(" %s --programs|-p\n", ownName) + fmt.Printf("Syntax: %s [arguments]\n", ownName) + fmt.Printf(" %s --plugin|-p\n", ownName) fmt.Printf(" %s --version|-v", ownName) } switch os.Args[1] { - case "--programs", "-p": - for name := range programs { + case "--plugins", "-p": + for name := range plugins { fmt.Println(name) } os.Exit(0) @@ -43,20 +44,30 @@ func getProgram() program.Program { os.Exit(0) } - if builder, ok := programs[os.Args[1]]; ok { + if builder, ok := plugins[os.Args[1]]; ok { os.Args = os.Args[1:] return builder() } - fmt.Printf("Unknown program: %s\n", os.Args[1]) + fmt.Printf("Unknown plugin: %s\n", os.Args[1]) os.Exit(1) return nil } func main() { - program := getProgram() - defer program.Done() - if program.CheckArguments() { - program.RunCheck() + runPlugin := getPlugin() + + output := runPlugin.Results() + defer func() { + if r := recover(); r != nil { + output.SetState(results.StatusUnknown, "Internal error") + output.AddLinef("Error info: %v", r) + } + fmt.Println(output.String()) + os.Exit(output.ExitCode()) + }() + + if runPlugin.CheckArguments() { + runPlugin.RunCheck() } } diff --git a/pkg/perfdata/internals.go b/pkg/perfdata/internals.go new file mode 100644 index 0000000..0f5f0da --- /dev/null +++ b/pkg/perfdata/internals.go @@ -0,0 +1,25 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "regexp" +) + +// Flags indicating which elements of performance data have been set. +type perfDataBits int + +const ( + PDatWarn perfDataBits = 1 << iota + PDatCrit + PDatMin + PDatMax +) + +// Regexps used to check values and ranges in performance data records. +var ( + // Common value check regexp. + vcRegexp = `^-?(0(\.\d*)?|[1-9]\d*(\.\d*)?|\.\d+)$` + // Compiled value check regexp. + valueCheck = regexp.MustCompile(vcRegexp) + // Compiled range min value check. + rangeMinCheck = regexp.MustCompile(vcRegexp + `|^~$`) +) diff --git a/pkg/perfdata/internals_test.go b/pkg/perfdata/internals_test.go new file mode 100644 index 0000000..8f77c52 --- /dev/null +++ b/pkg/perfdata/internals_test.go @@ -0,0 +1,50 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValueCheckValid(t *testing.T) { + validValues := []string{ + "0", "0.", "0.952", "1", "123", "123.", "123.45", ".1", + "-0", "-0.", "-0.952", "-1", "-123", "-123.", "-123.45", "-.1", + } + + for _, value := range validValues { + assert.True(t, valueCheck.MatchString(value), "'%s' is a valid value string", value) + } +} + +func TestValueCheckInvalid(t *testing.T) { + invalidValues := []string{".", "-.", "a", " ", "", "~"} + + for _, value := range invalidValues { + assert.False(t, valueCheck.MatchString(value), "'%s' is an invalid value string", value) + } +} + +func TestMinCheckValid(t *testing.T) { + validValues := []string{ + "0", "0.", "0.952", "1", "123", "123.", "123.45", ".1", + "-0", "-0.", "-0.952", "-1", "-123", "-123.", "-123.45", "-.1", + "~", + } + + for _, value := range validValues { + if !rangeMinCheck.MatchString(value) { + t.Errorf("'%s' is a valid value string", value) + } + } +} + +func TestMinCheckInvalid(t *testing.T) { + invalidValues := []string{".", "-.", "a", " ", ""} + + for _, value := range invalidValues { + if rangeMinCheck.MatchString(value) { + t.Errorf("'%s' is an invalid value string", value) + } + } +} diff --git a/pkg/perfdata/perfdata.go b/pkg/perfdata/perfdata.go index bc7195b..fc1914e 100644 --- a/pkg/perfdata/perfdata.go +++ b/pkg/perfdata/perfdata.go @@ -3,111 +3,9 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( - "fmt" - "regexp" "strings" ) -// Units of measurement, which may be used to qualify the performance data. -type UnitOfMeasurement int - -const ( - UomNone UnitOfMeasurement = iota - UomSeconds - UomPercent - UomBytes - UomKilobytes - UomMegabytes - UomGigabytes - UomTerabytes - UomCounter -) - -func (u UnitOfMeasurement) String() string { - return [...]string{"", "s", "%", "B", "KB", "MB", "GB", "TB", "c"}[u] -} - -// Flags indicating which elements of performance data have been set. -type perfDataBits int - -const ( - PDatWarn perfDataBits = 1 << iota - PDatCrit - PDatMin - PDatMax -) - -// Regexps used to check values and ranges in performance data records. -var ( - // Common value check regexp. - vcRegexp = `^-?(0(\.\d*)?|[1-9]\d*(\.\d*)?|\.\d+)$` - // Compiled value check regexp. - valueCheck = regexp.MustCompile(vcRegexp) - // Compiled range min value check. - rangeMinCheck = regexp.MustCompile(vcRegexp + `|^~$`) -) - -// Performance data range. -type PDRange struct { - start string - end string - inside bool -} - -// Creates a performance data range from -inf to 0 and from the specified -// value to +inf. -func PDRMax(max string) *PDRange { - if !valueCheck.MatchString(max) { - panic("invalid performance data range maximum value") - } - pdRange := &PDRange{} - pdRange.start = "0" - pdRange.end = max - return pdRange -} - -// Creates a performance data range from -inf to the specified minimal value -// and from the specified maximal value to +inf. -func PDRMinMax(min, max string) *PDRange { - if !valueCheck.MatchString(max) { - panic("invalid performance data range maximum value") - } - if !rangeMinCheck.MatchString(min) { - panic("invalid performance data range minimum value") - } - pdRange := &PDRange{} - pdRange.start = min - pdRange.end = max - return pdRange -} - -// Inverts the range. -func (r *PDRange) Inside() *PDRange { - r.inside = true - return r -} - -// Generates the range's string representation so it can be sent to the -// monitoring system. -func (r *PDRange) String() string { - var start, inside string - - switch r.start { - case "": - start = "~" - case "0": - start = "" - default: - start = r.start - } - - if r.inside { - inside = "@" - } - - return inside + start + ":" + r.end -} - // Performance data, including a label, units, a value, warning/critical // ranges and min/max boundaries. type PerfData struct { @@ -115,7 +13,7 @@ type PerfData struct { units UnitOfMeasurement bits perfDataBits value string - warn, crit PDRange + warn, crit Range min, max string } @@ -136,13 +34,13 @@ func New(label string, units UnitOfMeasurement, value string) *PerfData { } // Set the warning range for the performance data record. -func (d *PerfData) SetWarn(r *PDRange) { +func (d *PerfData) SetWarn(r *Range) { d.warn = *r d.bits |= PDatWarn } // Set the critical range for the performance data record. -func (d *PerfData) SetCrit(r *PDRange) { +func (d *PerfData) SetCrit(r *Range) { d.crit = *r d.bits |= PDatCrit } @@ -171,26 +69,28 @@ func (d *PerfData) String() string { var strBuilder strings.Builder needsQuotes := strings.ContainsAny(d.Label, " '=\"") if needsQuotes { - strBuilder.WriteString("'") + strBuilder.WriteRune('\'') } strBuilder.WriteString(strings.ReplaceAll(d.Label, "'", "''")) if needsQuotes { - strBuilder.WriteString("'") + strBuilder.WriteRune('\'') } - strBuilder.WriteString("=") - strBuilder.WriteString(fmt.Sprintf("%s%s;", d.value, d.units.String())) + strBuilder.WriteRune('=') + strBuilder.WriteString(d.value) + strBuilder.WriteString(d.units.String()) + strBuilder.WriteRune(';') if d.bits&PDatWarn != 0 { strBuilder.WriteString(d.warn.String()) } - strBuilder.WriteString(";") + strBuilder.WriteRune(';') if d.bits&PDatCrit != 0 { strBuilder.WriteString(d.crit.String()) } - strBuilder.WriteString(";") + strBuilder.WriteRune(';') if d.bits&PDatMin != 0 { strBuilder.WriteString(d.min) } - strBuilder.WriteString(";") + strBuilder.WriteRune(';') if d.bits&PDatMax != 0 { strBuilder.WriteString(d.max) } diff --git a/pkg/perfdata/perfdata_test.go b/pkg/perfdata/perfdata_test.go index 0c0fe2f..894077c 100644 --- a/pkg/perfdata/perfdata_test.go +++ b/pkg/perfdata/perfdata_test.go @@ -1,108 +1,261 @@ -package perfdata // import nocternity.net/gomonop/pkg/perfdata +package perfdata import ( "fmt" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func assert(t *testing.T, check bool, msg string) { - if !check { - t.Errorf(msg) - } +func TestNewNoValue(t *testing.T) { + const label = "label" + const units = UomNone + + out := New(label, units, "") + + assert.Equal(t, label, out.Label) + assert.Equal(t, units, out.units) + assert.Equal(t, perfDataBits(0), out.bits) + assert.Equal(t, "U", out.value) + // min, max, warn and crit are meaningless here } -func assertPanic(t *testing.T, f func(), msg string) { - defer func() { - if r := recover(); r == nil { - t.Errorf(msg) - } - }() - f() +func TestNewValidValue(t *testing.T) { + const label = "label" + const units = UomNone + const value = "1234" + + out := New(label, units, value) + + assert.Equal(t, label, out.Label) + assert.Equal(t, units, out.units) + assert.Equal(t, perfDataBits(0), out.bits) + assert.Equal(t, value, out.value) + // min, max, warn and crit are meaningless here } -func TestValueCheckValid(t *testing.T) { - validValues := []string{ - "0", "0.", "0.952", "1", "123", "123.", "123.45", ".1", - "-0", "-0.", "-0.952", "-1", "-123", "-123.", "-123.45", "-.1", - } - - for _, value := range validValues { - if !valueCheck.MatchString(value) { - t.Errorf("'%s' is a valid value string", value) - } - } +func TestNewInvalidValue(t *testing.T) { + assert.Panics(t, func() { New("label", UomNone, "nope") }) } -func TestValueCheckInvalid(t *testing.T) { - invalidValues := []string{".", "-.", "a", " ", ""} +func TestSetWarn(t *testing.T) { + rangeValue := Range{start: "A", end: "B"} + rangeStr := rangeValue.String() - for _, value := range invalidValues { - if valueCheck.MatchString(value) { - t.Errorf("'%s' is an invalid value string", value) - } - } + data := PerfData{} + data.SetWarn(&rangeValue) + + assert.True(t, data.bits&PDatWarn != 0) + assert.Equal(t, rangeStr, data.warn.String()) } -func TestPdrMaxInvalid(t *testing.T) { - assertPanic( - t, func() { PDRMax("") }, - "Created PerfDataRange with invalid max value", - ) +func TestSetWarnTwice(t *testing.T) { + range1Value := Range{start: "A", end: "B"} + range2Value := Range{start: "C", end: "D"} + range2Str := range2Value.String() + require.NotEqual(t, range2Str, range1Value.String()) + + data := PerfData{} + data.SetWarn(&range1Value) + data.SetWarn(&range2Value) + + assert.True(t, data.bits&PDatWarn != 0) + assert.Equal(t, range2Str, data.warn.String()) } -func TestPdrMax(t *testing.T) { - value := "123" - pdr := PDRMax(value) - assert(t, pdr.start == "0", "Min value should be '0'") - assert(t, pdr.end == value, "Max value not copied to PerfDataRange") - assert(t, !pdr.inside, "Inside flag should not be set") +func TestSetCrit(t *testing.T) { + rangeValue := Range{start: "A", end: "B"} + rangeStr := rangeValue.String() + + data := PerfData{} + data.SetCrit(&rangeValue) + + assert.True(t, data.bits&PDatCrit != 0) + assert.Equal(t, rangeStr, data.crit.String()) } -func TestPdrMinMaxInvalid(t *testing.T) { - assertPanic( - t, func() { PDRMinMax("", "123") }, - "Created PerfDataRange with invalid min value", - ) - assertPanic( - t, func() { PDRMinMax("123", "") }, - "Created PerfDataRange with invalid max value", - ) +func TestSetCritTwice(t *testing.T) { + range1Value := Range{start: "A", end: "B"} + range2Value := Range{start: "C", end: "D"} + range2Str := range2Value.String() + require.NotEqual(t, range2Str, range1Value.String()) + + data := PerfData{} + data.SetCrit(&range1Value) + data.SetCrit(&range2Value) + + assert.True(t, data.bits&PDatCrit != 0) + assert.Equal(t, range2Str, data.crit.String()) } -func TestPdrMinMax(t *testing.T) { - min, max := "123", "456" - pdr := PDRMinMax(min, max) - assert(t, pdr.start == min, "Min value not copied to PerfDataRange") - assert(t, pdr.end == max, "Max value not copied to PerfDataRange") - assert(t, !pdr.inside, "Inside flag should not be set") +func TestSetMin(t *testing.T) { + const min = "100" + + data := PerfData{} + data.SetMin(min) + + assert.True(t, data.bits&PDatMin != 0) + assert.Equal(t, min, data.min) } -func TestPdrInside(t *testing.T) { - pdr := &PDRange{} - pdr = pdr.Inside() - assert(t, pdr.inside, "Inside flag should be set") - pdr = pdr.Inside() - assert(t, pdr.inside, "Inside flag should still be set") +func TestSetMinInvalid(t *testing.T) { + data := PerfData{} + assert.Panics(t, func() { data.SetMin("nope") }) } -func TestPdrString(t *testing.T) { +func TestSetMinTwice(t *testing.T) { + data := PerfData{} + data.SetMin("100") + data.SetMin("200") + assert.Equal(t, "200", data.min) + assert.True(t, data.bits&PDatMin != 0) +} + +func TestSetMax(t *testing.T) { + const max = "100" + + data := PerfData{} + data.SetMax(max) + + assert.True(t, data.bits&PDatMax != 0) + assert.Equal(t, max, data.max) +} + +func TestSetMaxInvalid(t *testing.T) { + data := PerfData{} + assert.Panics(t, func() { data.SetMax("nope") }) +} + +func TestSetMaxTwice(t *testing.T) { + data := PerfData{} + data.SetMax("100") + data.SetMax("200") + assert.Equal(t, "200", data.max) + assert.True(t, data.bits&PDatMax != 0) +} + +func TestString(t *testing.T) { type Test struct { - pdr PDRange - out string + PerfData + expected string } + + range1 := Range{start: "A", end: "B"} + range2 := Range{start: "C", end: "D"} + tests := []Test{ - {pdr: PDRange{start: "Y", end: "X"}, out: "Y:X"}, - {pdr: PDRange{end: "X"}, out: "~:X"}, - {pdr: PDRange{start: "0", end: "X"}, out: ":X"}, - {pdr: PDRange{inside: true, start: "Y", end: "X"}, out: "@Y:X"}, + { + PerfData{ + Label: "label", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "label=1234;;;;", + }, + { + PerfData{ + Label: "la=bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la=bel'=1234;;;;", + }, + { + PerfData{ + Label: "la bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la bel'=1234;;;;", + }, + { + PerfData{ + Label: "la\"bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la\"bel'=1234;;;;", + }, + { + PerfData{ + Label: "la'bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la''bel'=1234;;;;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatWarn, + value: "1234", + warn: range1, + }, + "label=1234;" + range1.String() + ";;;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatCrit, + value: "1234", + crit: range1, + }, + "label=1234;;" + range1.String() + ";;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatWarn | PDatCrit, + value: "1234", + warn: range1, + crit: range2, + }, + "label=1234;" + range1.String() + ";" + range2.String() + ";;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatMin, + value: "1234", + min: "X", + }, + "label=1234;;;X;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatMax, + value: "1234", + max: "Y", + }, + "label=1234;;;;Y", + }, + } + + for _, units := range []UnitOfMeasurement{UomSeconds, UomPercent, UomBytes, UomKilobytes, UomMegabytes, UomGigabytes, UomTerabytes, UomCounter} { + tests = append(tests, Test{ + PerfData{ + Label: "label", + units: units, + bits: perfDataBits(0), + value: "1234", + }, + fmt.Sprintf("label=1234%s;;;;", units), + }) } for _, test := range tests { - result := test.pdr.String() - assert( - t, - result == test.out, - fmt.Sprintf("Expected '%s', got '%s'", test.out, result), - ) + assert.Equal(t, test.expected, test.PerfData.String()) } } diff --git a/pkg/perfdata/range.go b/pkg/perfdata/range.go new file mode 100644 index 0000000..e4ad2c7 --- /dev/null +++ b/pkg/perfdata/range.go @@ -0,0 +1,62 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +// Performance data range. +type Range struct { + start string + end string + inside bool +} + +// Creates a performance data range from -inf to 0 and from the specified +// value to +inf. +func RangeMax(max string) *Range { + if !valueCheck.MatchString(max) { + panic("invalid performance data range maximum value") + } + pdRange := &Range{} + pdRange.start = "0" + pdRange.end = max + return pdRange +} + +// Creates a performance data range from -inf to the specified minimal value +// and from the specified maximal value to +inf. +func RangeMinMax(min, max string) *Range { + if !valueCheck.MatchString(max) { + panic("invalid performance data range maximum value") + } + if !rangeMinCheck.MatchString(min) { + panic("invalid performance data range minimum value") + } + pdRange := &Range{} + pdRange.start = min + pdRange.end = max + return pdRange +} + +// Inverts the range. +func (r *Range) Inside() *Range { + r.inside = true + return r +} + +// Generates the range's string representation so it can be sent to the +// monitoring system. +func (r *Range) String() string { + var start, inside string + + switch r.start { + case "": + start = "~" + case "0": + start = "" + default: + start = r.start + } + + if r.inside { + inside = "@" + } + + return inside + start + ":" + r.end +} diff --git a/pkg/perfdata/range_test.go b/pkg/perfdata/range_test.go new file mode 100644 index 0000000..26d1f21 --- /dev/null +++ b/pkg/perfdata/range_test.go @@ -0,0 +1,67 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRangeMaxInvalid(t *testing.T) { + assert.Panics( + t, func() { RangeMax("") }, + "Created PerfDataRange with invalid max value", + ) +} + +func TestRangeMax(t *testing.T) { + value := "123" + pdr := RangeMax(value) + assert.Equal(t, "0", pdr.start, "Min value should be '0'") + assert.Equal(t, value, pdr.end, "Max value not copied to PerfDataRange") + assert.False(t, pdr.inside, "Inside flag should not be set") +} + +func TestRangeMinMaxInvalid(t *testing.T) { + assert.Panics( + t, func() { RangeMinMax("", "123") }, + "Created PerfDataRange with invalid min value", + ) + assert.Panics( + t, func() { RangeMinMax("123", "") }, + "Created PerfDataRange with invalid max value", + ) +} + +func TestRangeMinMax(t *testing.T) { + min, max := "123", "456" + pdr := RangeMinMax(min, max) + assert.Equal(t, min, pdr.start, "Min value not copied to PerfDataRange") + assert.Equal(t, max, pdr.end, "Max value not copied to PerfDataRange") + assert.False(t, pdr.inside, "Inside flag should not be set") +} + +func TestRangeInside(t *testing.T) { + pdr := &Range{} + pdr = pdr.Inside() + assert.True(t, pdr.inside, "Inside flag should be set") + pdr = pdr.Inside() + assert.True(t, pdr.inside, "Inside flag should still be set") +} + +func TestRangeString(t *testing.T) { + type Test struct { + pdr Range + out string + } + tests := []Test{ + {pdr: Range{start: "Y", end: "X"}, out: "Y:X"}, + {pdr: Range{end: "X"}, out: "~:X"}, + {pdr: Range{start: "0", end: "X"}, out: ":X"}, + {pdr: Range{inside: true, start: "Y", end: "X"}, out: "@Y:X"}, + } + + for _, test := range tests { + result := test.pdr.String() + assert.Equal(t, test.out, result, "Expected '%s', got '%s'", test.out, result) + } +} diff --git a/pkg/perfdata/units.go b/pkg/perfdata/units.go new file mode 100644 index 0000000..604625c --- /dev/null +++ b/pkg/perfdata/units.go @@ -0,0 +1,20 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +// Units of measurement, which may be used to qualify the performance data. +type UnitOfMeasurement int + +const ( + UomNone UnitOfMeasurement = iota + UomSeconds + UomPercent + UomBytes + UomKilobytes + UomMegabytes + UomGigabytes + UomTerabytes + UomCounter +) + +func (u UnitOfMeasurement) String() string { + return [...]string{"", "s", "%", "B", "KB", "MB", "GB", "TB", "c"}[u] +} diff --git a/pkg/perfdata/units_test.go b/pkg/perfdata/units_test.go new file mode 100644 index 0000000..b5b56a5 --- /dev/null +++ b/pkg/perfdata/units_test.go @@ -0,0 +1,26 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnitsToString(t *testing.T) { + checks := map[UnitOfMeasurement]string{ + UomNone: "", + UomSeconds: "s", + UomPercent: "%", + UomBytes: "B", + UomKilobytes: "KB", + UomMegabytes: "MB", + UomGigabytes: "GB", + UomTerabytes: "TB", + UomCounter: "c", + } + + for u, s := range checks { + result := u.String() + assert.Equal(t, s, result, "Expected '%s', got '%s'", s, result) + } +} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index f32e360..a60366f 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -1,122 +1,20 @@ -// Package plugin implements a helper that can be used to implement a Nagios, -// Centreon, Icinga... service monitoring plugin. -package plugin // import nocternity.net/gomonop/pkg/perfdata +package plugin // import nocternity.net/gomonop/pkg/plugin -import ( - "container/list" - "fmt" - "os" - "strings" +import "nocternity.net/gomonop/pkg/results" - "nocternity.net/gomonop/pkg/perfdata" -) +// Plugin represents the interface to a monitoring plugin. +type Plugin interface { + // Results accesses the results of the monitoring plugin. + Results() *results.Results -// Status represents the return status of the monitoring plugin. The -// corresponding integer value will be used as the program's exit code, -// to be interpreted by the monitoring system. -type Status int + // CheckArguments ensures that the arguments that were passed to the plugin + // actually make sense. Errors should be stored in the plugin's results. + CheckArguments() bool -// Plugin exit statuses. -const ( - OK Status = iota - WARNING - CRITICAL - UNKNOWN -) - -// String representations of the plugin statuses. -func (s Status) String() string { - return [...]string{"OK", "WARNING", "ERROR", "UNKNOWN"}[s] + // RunCheck actually runs whatever checks are implemented by the plugin and + // updates the results accordingly. + RunCheck() } -// Plugin represents the monitoring plugin's state, including its name, -// return status and message, additional lines of text, and performance -// data to be encoded in the output. -type Plugin struct { - name string - status Status - message string - extraText *list.List - perfData map[string]*perfdata.PerfData -} - -// New creates the plugin with `name` as its name and an unknown status. -func New(name string) *Plugin { - p := new(Plugin) - p.name = name - p.status = UNKNOWN - p.message = "no status set" - p.perfData = make(map[string]*perfdata.PerfData) - return p -} - -// SetState sets the plugin's output code to `status` and its message to -// the specified `message`. -func (p *Plugin) SetState(status Status, message string) { - p.status = status - p.message = message -} - -// AddLine adds the specified string to the extra output text buffer. -func (p *Plugin) AddLine(line string) { - if p.extraText == nil { - p.extraText = list.New() - } - p.extraText.PushBack(line) -} - -// AddLinef formats the input and adds it to the text buffer. -func (p *Plugin) AddLinef(format string, data ...interface{}) { - p.AddLine(fmt.Sprintf(format, data...)) -} - -// AddLines add the specified `lines` to the output text. -func (p *Plugin) AddLines(lines []string) { - for _, line := range lines { - p.AddLine(line) - } -} - -// AddPerfData adds performance data described by the "pd" argument to the -// output's performance data. If two performance data records are added for -// the same label, the program panics. -func (p *Plugin) AddPerfData(pd *perfdata.PerfData) { - _, exists := p.perfData[pd.Label] - if exists { - panic("duplicate performance data " + pd.Label) - } - p.perfData[pd.Label] = pd -} - -// Done generates the plugin's text output from its name, status, text data -// and performance data, before exiting with the code corresponding to the -// status. -func (p *Plugin) Done() { - var strBuilder strings.Builder - strBuilder.WriteString(p.name) - strBuilder.WriteString(" ") - strBuilder.WriteString(p.status.String()) - strBuilder.WriteString(": ") - strBuilder.WriteString(p.message) - if len(p.perfData) > 0 { - strBuilder.WriteString(" | ") - needSep := false - for _, data := range p.perfData { - if needSep { - strBuilder.WriteString(", ") - } else { - needSep = true - } - strBuilder.WriteString(data.String()) - } - } - if p.extraText != nil { - for em := p.extraText.Front(); em != nil; em = em.Next() { - strBuilder.WriteString("\n") - //nolint:forcetypeassert // we want to panic if this isn't a string - strBuilder.WriteString(em.Value.(string)) - } - } - fmt.Println(strBuilder.String()) - os.Exit(int(p.status)) -} +// Builder is a function that can be called in order to instantiate a plugin. +type Builder func() Plugin diff --git a/pkg/program/program.go b/pkg/program/program.go deleted file mode 100644 index d8b3510..0000000 --- a/pkg/program/program.go +++ /dev/null @@ -1,9 +0,0 @@ -package program // import nocternity.net/gomonop/pkg/program - -type Program interface { - CheckArguments() bool - RunCheck() - Done() -} - -type Builder func() Program diff --git a/pkg/results/results.go b/pkg/results/results.go new file mode 100644 index 0000000..3118071 --- /dev/null +++ b/pkg/results/results.go @@ -0,0 +1,107 @@ +// Package results implements a helper that can be used to store the results of +// a Nagios, Centreon, Icinga... service monitoring plugin, and convert them to +// text which can be sent to the monitoring server. +package results // import nocternity.net/gomonop/pkg/results + +import ( + "container/list" + "fmt" + "strings" + + "nocternity.net/gomonop/pkg/perfdata" +) + +// Results represents the monitoring plugin's results, including its name, +// return status and message, additional lines of text, and performance +// data to be encoded in the output. +type Results struct { + name string + status Status + message string + extraText *list.List + perfData map[string]*perfdata.PerfData +} + +// New creates the plugin with `name` as its name and an unknown status. +func New(name string) *Results { + p := new(Results) + p.name = name + p.status = StatusUnknown + p.message = "no status set" + p.perfData = make(map[string]*perfdata.PerfData) + return p +} + +// SetState sets the plugin's output code to `status` and its message to +// the specified `message`. +func (p *Results) SetState(status Status, message string) { + p.status = status + p.message = message +} + +// AddLine adds the specified string to the extra output text buffer. +func (p *Results) AddLine(line string) { + if p.extraText == nil { + p.extraText = list.New() + } + p.extraText.PushBack(line) +} + +// AddLinef formats the input and adds it to the text buffer. +func (p *Results) AddLinef(format string, data ...interface{}) { + p.AddLine(fmt.Sprintf(format, data...)) +} + +// AddLines add the specified `lines` to the output text. +func (p *Results) AddLines(lines []string) { + for _, line := range lines { + p.AddLine(line) + } +} + +// AddPerfData adds performance data described by the "pd" argument to the +// output's performance data. If two performance data records are added for +// the same label, the program panics. +func (p *Results) AddPerfData(pd *perfdata.PerfData) { + _, exists := p.perfData[pd.Label] + if exists { + panic("duplicate performance data " + pd.Label) + } + p.perfData[pd.Label] = pd +} + +// String generates the plugin's text output from its name, status, text data +// and performance data. +func (p *Results) String() string { + var strBuilder strings.Builder + strBuilder.WriteString(p.name) + strBuilder.WriteString(" ") + strBuilder.WriteString(p.status.String()) + strBuilder.WriteString(": ") + strBuilder.WriteString(p.message) + if len(p.perfData) > 0 { + strBuilder.WriteString(" | ") + needSep := false + for _, data := range p.perfData { + if needSep { + strBuilder.WriteString(", ") + } else { + needSep = true + } + strBuilder.WriteString(data.String()) + } + } + if p.extraText != nil { + for em := p.extraText.Front(); em != nil; em = em.Next() { + strBuilder.WriteString("\n") + //nolint:forcetypeassert // we want to panic if this isn't a string + strBuilder.WriteString(em.Value.(string)) + } + } + return strBuilder.String() +} + +// ExitCode returns the plugin's exit code. +func (p *Results) ExitCode() int { + return int(p.status) +} diff --git a/pkg/results/results_test.go b/pkg/results/results_test.go new file mode 100644 index 0000000..32ddacf --- /dev/null +++ b/pkg/results/results_test.go @@ -0,0 +1,175 @@ +package results // import nocternity.net/gomonop/pkg/results + +import ( + "container/list" + "testing" + + "github.com/stretchr/testify/assert" + "nocternity.net/gomonop/pkg/perfdata" +) + +func TestNew(t *testing.T) { + p := New("test") + + assert.Equal(t, p.name, "test") + assert.Equal(t, p.status, StatusUnknown) + assert.Equal(t, p.message, "no status set") + assert.Nil(t, p.extraText) + assert.NotNil(t, p.perfData) +} + +func TestSetState(t *testing.T) { + p := Results{} + + p.SetState(StatusWarning, "test") + + assert.Equal(t, p.status, StatusWarning) + assert.Equal(t, p.message, "test") +} + +func TestAddLineFirst(t *testing.T) { + p := Results{} + + p.AddLine("test") + + assert.NotNil(t, p.extraText) + assert.Equal(t, p.extraText.Len(), 1) + assert.Equal(t, p.extraText.Front().Value, "test") +} + +func TestAddLineNext(t *testing.T) { + p := Results{} + p.extraText = list.New() + + p.AddLine("test") + + assert.Equal(t, p.extraText.Len(), 1) + assert.Equal(t, p.extraText.Front().Value, "test") +} + +func TestAddLinef(t *testing.T) { + p := Results{} + + p.AddLinef("test %d", 123) + + assert.Equal(t, p.extraText.Len(), 1) + assert.Equal(t, p.extraText.Front().Value, "test 123") +} + +func TestAddLines(t *testing.T) { + p := Results{} + + p.AddLines([]string{"test", "test2"}) + + assert.Equal(t, p.extraText.Len(), 2) + assert.Equal(t, p.extraText.Front().Value, "test") + assert.Equal(t, p.extraText.Front().Next().Value, "test2") +} + +func TestAddPerfData(t *testing.T) { + p := Results{} + p.perfData = make(map[string]*perfdata.PerfData) + + p.AddPerfData(&perfdata.PerfData{Label: "test"}) + + value, ok := p.perfData["test"] + assert.True(t, ok) + assert.Equal(t, value.Label, "test") +} + +func TestAddPerfDataDuplicate(t *testing.T) { + p := Results{} + p.perfData = make(map[string]*perfdata.PerfData) + p.perfData["test"] = &perfdata.PerfData{Label: "test"} + + assert.Panics(t, func() { p.AddPerfData(&perfdata.PerfData{Label: "test"}) }) +} + +func TestString(t *testing.T) { + type Test struct { + Results + expected string + } + + pdat := perfdata.PerfData{Label: "test"} + tests := []Test{ + { + Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + }, + "test WARNING: test", + }, + { + func() Results { + p := Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + extraText: list.New(), + } + p.extraText.PushBack("test 1") + p.extraText.PushBack("test 2") + return p + }(), + "test WARNING: test\ntest 1\ntest 2", + }, + { + func() Results { + p := Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + } + p.perfData["test 1"] = &pdat + p.perfData["test 2"] = &pdat + return p + }(), + "test WARNING: test | " + pdat.String() + ", " + + pdat.String(), + }, + { + func() Results { + p := Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + extraText: list.New(), + } + p.perfData["test 1"] = &pdat + p.perfData["test 2"] = &pdat + p.extraText.PushBack("test 1") + p.extraText.PushBack("test 2") + return p + }(), + "test WARNING: test | " + pdat.String() + ", " + + pdat.String() + "\ntest 1\ntest 2", + }, + } + + for _, test := range tests { + result := test.Results.String() + assert.Equal(t, test.expected, result, "Expected '%s', got '%s'", test.expected, result) + } +} + +func TestExitCode(t *testing.T) { + p := Results{} + + p.status = StatusOK + assert.Equal(t, int(StatusOK), p.ExitCode()) + + p.status = StatusWarning + assert.Equal(t, int(StatusWarning), p.ExitCode()) + + p.status = StatusCritical + assert.Equal(t, int(StatusCritical), p.ExitCode()) + + p.status = StatusUnknown + assert.Equal(t, int(StatusUnknown), p.ExitCode()) +} diff --git a/pkg/results/status.go b/pkg/results/status.go new file mode 100644 index 0000000..6238a20 --- /dev/null +++ b/pkg/results/status.go @@ -0,0 +1,19 @@ +package results // import nocternity.net/gomonop/pkg/results + +// Status represents the return status of the monitoring plugin. The +// corresponding integer value will be used as the program's exit code, +// to be interpreted by the monitoring system. +type Status int + +// Plugin exit statuses. +const ( + StatusOK Status = iota + StatusWarning + StatusCritical + StatusUnknown +) + +// String representations of the plugin statuses. +func (s Status) String() string { + return [...]string{"OK", "WARNING", "ERROR", "UNKNOWN"}[s] +} diff --git a/pkg/results/status_test.go b/pkg/results/status_test.go new file mode 100644 index 0000000..8f81f43 --- /dev/null +++ b/pkg/results/status_test.go @@ -0,0 +1,26 @@ +package results // import nocternity.net/gomonop/pkg/results + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStatusDefaultOK(t *testing.T) { + var s Status + assert.Equal(t, StatusOK, s) +} + +func TestStatusToString(t *testing.T) { + assert.Equal(t, "OK", StatusOK.String()) + assert.Equal(t, "WARNING", StatusWarning.String()) + assert.Equal(t, "ERROR", StatusCritical.String()) + assert.Equal(t, "UNKNOWN", StatusUnknown.String()) +} + +func TestStatusToInt(t *testing.T) { + assert.Equal(t, 0, int(StatusOK)) + assert.Equal(t, 1, int(StatusWarning)) + assert.Equal(t, 2, int(StatusCritical)) + assert.Equal(t, 3, int(StatusUnknown)) +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 0000000..17f96e4 --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,31 @@ +package version // import nocternity.net/gomonop/pkg/version + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVersion(t *testing.T) { + type Test struct { + version, commit, status, target, expected string + } + + tests := []Test{ + {"", "COMMIT", "clean", "TARGET", "development version (COMMIT) TARGET"}, + {"VERSION", "COMMIT", "clean", "TARGET", "VERSION (COMMIT) TARGET"}, + {"", "COMMIT", "dirty", "TARGET", "development version (COMMIT*) TARGET"}, + {"VERSION", "COMMIT", "dirty", "TARGET", "VERSION (COMMIT*) TARGET"}, + } + + for _, test := range tests { + version = test.version + commit = test.commit + status = test.status + target = test.target + + result := Version() + + assert.Equal(t, test.expected, result, "Expected '%s', got '%s'", test.expected, result) + } +}