refactor: make internals easier to test and add unit tests (#2)
All checks were successful
Run tests and linters / test (push) Successful in 44s
Run tests and linters / build (push) Successful in 47s
Run tests and linters / lint (push) Successful in 1m20s

This PR refactors most of the internals to make them easier to test (and also because the names didn't make sense). It adds unit tests for all internal components.

Reviewed-on: #2
Co-authored-by: Emmanuel BENOÎT <tseeker@nocternity.net>
Co-committed-by: Emmanuel BENOÎT <tseeker@nocternity.net>
This commit is contained in:
Emmanuel BENOîT 2024-07-20 10:01:05 +02:00 committed by Emmanuel BENOîT
parent dcd732cc34
commit 78af496fe9
20 changed files with 939 additions and 369 deletions

View file

@ -16,7 +16,7 @@ import (
"nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/perfdata"
"nocternity.net/gomonop/pkg/plugin" "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. // Program data including configuration and runtime data.
type checkProgram struct { type checkProgram struct {
programFlags // Flags from the command line 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 certificate *x509.Certificate // X.509 certificate from the server
} }
@ -207,37 +207,37 @@ func (flags *programFlags) parseArguments() {
} }
// Initialise the monitoring check program. // Initialise the monitoring check program.
func NewProgram() program.Program { func NewProgram() plugin.Plugin {
program := &checkProgram{ program := &checkProgram{
plugin: plugin.New("Certificate check"), plugin: results.New("Certificate check"),
} }
program.parseArguments() program.parseArguments()
return program return program
} }
// Terminate the monitoring check program. // Return the program's output value.
func (program *checkProgram) Done() { func (program *checkProgram) Results() *results.Results {
program.plugin.Done() return program.plugin
} }
// Check the values that were specified from the command line. Returns true // Check the values that were specified from the command line. Returns true
// if the arguments made sense. // if the arguments made sense.
func (program *checkProgram) CheckArguments() bool { func (program *checkProgram) CheckArguments() bool {
if program.hostname == "" { if program.hostname == "" {
program.plugin.SetState(plugin.UNKNOWN, "no hostname specified") program.plugin.SetState(results.StatusUnknown, "no hostname specified")
return false return false
} }
if program.port < 1 || program.port > 65535 { 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 return false
} }
if program.warn != -1 && program.crit != -1 && program.warn <= program.crit { 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 return false
} }
if _, ok := certGetters[program.startTLS]; !ok { if _, ok := certGetters[program.startTLS]; !ok {
errstr := "unsupported StartTLS protocol " + program.startTLS errstr := "unsupported StartTLS protocol " + program.startTLS
program.plugin.SetState(plugin.UNKNOWN, errstr) program.plugin.SetState(results.StatusUnknown, errstr)
return false return false
} }
program.hostname = strings.ToLower(program.hostname) program.hostname = strings.ToLower(program.hostname)
@ -262,13 +262,13 @@ func (program *checkProgram) getCertificate() error {
// matches the requested host name. // matches the requested host name.
func (program *checkProgram) checkSANlessCertificate() bool { func (program *checkProgram) checkSANlessCertificate() bool {
if !program.ignoreCnOnly || len(program.extraNames) != 0 { if !program.ignoreCnOnly || len(program.extraNames) != 0 {
program.plugin.SetState(plugin.WARNING, program.plugin.SetState(results.StatusWarning,
"certificate doesn't have SAN domain names") "certificate doesn't have SAN domain names")
return false return false
} }
dn := strings.ToLower(program.certificate.Subject.String()) dn := strings.ToLower(program.certificate.Subject.String())
if !strings.HasPrefix(dn, fmt.Sprintf("cn=%s,", program.hostname)) { 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 false
} }
return true return true
@ -298,7 +298,7 @@ func (program *checkProgram) checkNames() bool {
certificateIsOk = program.checkHostName(name) && certificateIsOk certificateIsOk = program.checkHostName(name) && certificateIsOk
} }
if !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 return certificateIsOk
} }
@ -306,26 +306,26 @@ func (program *checkProgram) checkNames() bool {
// Check a certificate's time to expiry against the warning and critical // Check a certificate's time to expiry against the warning and critical
// thresholds, returning a status code and description based on these // thresholds, returning a status code and description based on these
// values. // values.
func (program *checkProgram) checkCertificateExpiry(tlDays int) (plugin.Status, string) { func (program *checkProgram) checkCertificateExpiry(tlDays int) (results.Status, string) {
if tlDays <= 0 { if tlDays <= 0 {
return plugin.CRITICAL, "certificate expired" return results.StatusCritical, "certificate expired"
} }
var limitStr string var limitStr string
var state plugin.Status var state results.Status
switch { switch {
case program.crit > 0 && tlDays <= program.crit: case program.crit > 0 && tlDays <= program.crit:
limitStr = fmt.Sprintf(" (<= %d)", program.crit) limitStr = fmt.Sprintf(" (<= %d)", program.crit)
state = plugin.CRITICAL state = results.StatusCritical
case program.warn > 0 && tlDays <= program.warn: case program.warn > 0 && tlDays <= program.warn:
limitStr = fmt.Sprintf(" (<= %d)", program.warn) limitStr = fmt.Sprintf(" (<= %d)", program.warn)
state = plugin.WARNING state = results.StatusWarning
default: default:
limitStr = "" limitStr = ""
state = plugin.OK state = results.StatusOK
} }
statusString := fmt.Sprintf("certificate will expire in %d days%s", 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) { func (program *checkProgram) setPerfData(tlDays int) {
pdat := perfdata.New("validity", perfdata.UomNone, strconv.Itoa(tlDays)) pdat := perfdata.New("validity", perfdata.UomNone, strconv.Itoa(tlDays))
if program.crit > 0 { if program.crit > 0 {
pdat.SetCrit(perfdata.PDRMax(strconv.Itoa(program.crit))) pdat.SetCrit(perfdata.RangeMax(strconv.Itoa(program.crit)))
} }
if program.warn > 0 { if program.warn > 0 {
pdat.SetWarn(perfdata.PDRMax(strconv.Itoa(program.warn))) pdat.SetWarn(perfdata.RangeMax(strconv.Itoa(program.warn)))
} }
program.plugin.AddPerfData(pdat) program.plugin.AddPerfData(pdat)
} }
@ -351,7 +351,7 @@ func (program *checkProgram) setPerfData(tlDays int) {
func (program *checkProgram) RunCheck() { func (program *checkProgram) RunCheck() {
err := program.getCertificate() err := program.getCertificate()
if err != nil { if err != nil {
program.plugin.SetState(plugin.UNKNOWN, err.Error()) program.plugin.SetState(results.StatusUnknown, err.Error())
} else if program.checkNames() { } else if program.checkNames() {
timeLeft := time.Until(program.certificate.NotAfter) timeLeft := time.Until(program.certificate.NotAfter)
tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour)) tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour))

View file

@ -14,7 +14,7 @@ import (
"nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/perfdata"
"nocternity.net/gomonop/pkg/plugin" "nocternity.net/gomonop/pkg/plugin"
"nocternity.net/gomonop/pkg/program" "nocternity.net/gomonop/pkg/results"
) )
//------------------------------------------------------------------------------------------------------- //-------------------------------------------------------------------------------------------------------
@ -56,7 +56,7 @@ type programFlags struct {
// Program data including configuration and runtime data. // Program data including configuration and runtime data.
type checkProgram struct { type checkProgram struct {
programFlags // Flags from the command line programFlags // Flags from the command line
plugin *plugin.Plugin // Plugin output state plugin *results.Results // Plugin output state
} }
// Parse command line arguments and store their values. If the -h flag is present, // 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. // Initialise the monitoring check program.
func NewProgram() program.Program { func NewProgram() plugin.Plugin {
program := &checkProgram{ program := &checkProgram{
plugin: plugin.New("DNS zone serial match check"), plugin: results.New("DNS zone serial match check"),
} }
program.parseArguments() program.parseArguments()
return program return program
} }
// Terminate the monitoring check program. // Return the program's output value.
func (program *checkProgram) Done() { func (program *checkProgram) Results() *results.Results {
if r := recover(); r != nil { return program.plugin
program.plugin.SetState(plugin.UNKNOWN, "Internal error")
program.plugin.AddLinef("Error info: %v", r)
}
program.plugin.Done()
} }
// Check the values that were specified from the command line. Returns true if the arguments made sense. // Check the values that were specified from the command line. Returns true if the arguments made sense.
func (program *checkProgram) CheckArguments() bool { func (program *checkProgram) CheckArguments() bool {
if program.hostname == "" { if program.hostname == "" {
program.plugin.SetState(plugin.UNKNOWN, "no DNS hostname specified") program.plugin.SetState(results.StatusUnknown, "no DNS hostname specified")
return false return false
} }
if program.port < 1 || program.port > 65535 { 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 return false
} }
if program.zone == "" { if program.zone == "" {
program.plugin.SetState(plugin.UNKNOWN, "no DNS zone specified") program.plugin.SetState(results.StatusUnknown, "no DNS zone specified")
return false return false
} }
if program.rsHostname == "" { 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 return false
} }
if program.rsPort < 1 || program.rsPort > 65535 { 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 return false
} }
program.hostname = strings.ToLower(program.hostname) program.hostname = strings.ToLower(program.hostname)
@ -180,12 +176,12 @@ func (program *checkProgram) RunCheck() {
cOk, cSerial := program.getSerial("checked", checkResponse) cOk, cSerial := program.getSerial("checked", checkResponse)
rOk, rSerial := program.getSerial("reference", refResponse) rOk, rSerial := program.getSerial("reference", refResponse)
if !(cOk && rOk) { if !(cOk && rOk) {
program.plugin.SetState(plugin.UNKNOWN, "could not read serials") program.plugin.SetState(results.StatusUnknown, "could not read serials")
return return
} }
if cSerial == rSerial { if cSerial == rSerial {
program.plugin.SetState(plugin.OK, "serials match") program.plugin.SetState(results.StatusOK, "serials match")
} else { } else {
program.plugin.SetState(plugin.CRITICAL, "serials mismatch") program.plugin.SetState(results.StatusCritical, "serials mismatch")
} }
} }

4
go.mod
View file

@ -8,7 +8,11 @@ require (
) )
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/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

9
go.sum
View file

@ -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 h1:9i9HnUh7uCyUFJhIqg311HBibw4f2pbGldi0ZM2FhaQ=
github.com/karrick/golf v1.4.0/go.mod h1:qGN0IhcEL+IEgCXp00RvH32UP59vtwc8w5YcIdArNRk= 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 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 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-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 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/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/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= 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=

39
main.go
View file

@ -7,33 +7,34 @@ import (
"nocternity.net/gomonop/cmd/sslcert" "nocternity.net/gomonop/cmd/sslcert"
"nocternity.net/gomonop/cmd/zoneserial" "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" "nocternity.net/gomonop/pkg/version"
) )
var ( 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_ssl_certificate": sslcert.NewProgram,
"check_zone_serial": zoneserial.NewProgram, "check_zone_serial": zoneserial.NewProgram,
} }
) )
func getProgram() program.Program { func getPlugin() plugin.Plugin {
ownName := filepath.Base(os.Args[0]) ownName := filepath.Base(os.Args[0])
if builder, ok := programs[ownName]; ok { if builder, ok := plugins[ownName]; ok {
return builder() return builder()
} }
if len(os.Args) < 2 { if len(os.Args) < 2 {
fmt.Printf("Syntax: %s <program> [arguments]\n", ownName) fmt.Printf("Syntax: %s <plugin> [arguments]\n", ownName)
fmt.Printf(" %s --programs|-p\n", ownName) fmt.Printf(" %s --plugin|-p\n", ownName)
fmt.Printf(" %s --version|-v", ownName) fmt.Printf(" %s --version|-v", ownName)
} }
switch os.Args[1] { switch os.Args[1] {
case "--programs", "-p": case "--plugins", "-p":
for name := range programs { for name := range plugins {
fmt.Println(name) fmt.Println(name)
} }
os.Exit(0) os.Exit(0)
@ -43,20 +44,30 @@ func getProgram() program.Program {
os.Exit(0) os.Exit(0)
} }
if builder, ok := programs[os.Args[1]]; ok { if builder, ok := plugins[os.Args[1]]; ok {
os.Args = os.Args[1:] os.Args = os.Args[1:]
return builder() return builder()
} }
fmt.Printf("Unknown program: %s\n", os.Args[1]) fmt.Printf("Unknown plugin: %s\n", os.Args[1])
os.Exit(1) os.Exit(1)
return nil return nil
} }
func main() { func main() {
program := getProgram() runPlugin := getPlugin()
defer program.Done()
if program.CheckArguments() { output := runPlugin.Results()
program.RunCheck() 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()
} }
} }

25
pkg/perfdata/internals.go Normal file
View file

@ -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 + `|^~$`)
)

View file

@ -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)
}
}
}

View file

@ -3,111 +3,9 @@
package perfdata // import nocternity.net/gomonop/pkg/perfdata package perfdata // import nocternity.net/gomonop/pkg/perfdata
import ( import (
"fmt"
"regexp"
"strings" "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 // Performance data, including a label, units, a value, warning/critical
// ranges and min/max boundaries. // ranges and min/max boundaries.
type PerfData struct { type PerfData struct {
@ -115,7 +13,7 @@ type PerfData struct {
units UnitOfMeasurement units UnitOfMeasurement
bits perfDataBits bits perfDataBits
value string value string
warn, crit PDRange warn, crit Range
min, max string 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. // 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.warn = *r
d.bits |= PDatWarn d.bits |= PDatWarn
} }
// Set the critical range for the performance data record. // 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.crit = *r
d.bits |= PDatCrit d.bits |= PDatCrit
} }
@ -171,26 +69,28 @@ func (d *PerfData) String() string {
var strBuilder strings.Builder var strBuilder strings.Builder
needsQuotes := strings.ContainsAny(d.Label, " '=\"") needsQuotes := strings.ContainsAny(d.Label, " '=\"")
if needsQuotes { if needsQuotes {
strBuilder.WriteString("'") strBuilder.WriteRune('\'')
} }
strBuilder.WriteString(strings.ReplaceAll(d.Label, "'", "''")) strBuilder.WriteString(strings.ReplaceAll(d.Label, "'", "''"))
if needsQuotes { if needsQuotes {
strBuilder.WriteString("'") strBuilder.WriteRune('\'')
} }
strBuilder.WriteString("=") strBuilder.WriteRune('=')
strBuilder.WriteString(fmt.Sprintf("%s%s;", d.value, d.units.String())) strBuilder.WriteString(d.value)
strBuilder.WriteString(d.units.String())
strBuilder.WriteRune(';')
if d.bits&PDatWarn != 0 { if d.bits&PDatWarn != 0 {
strBuilder.WriteString(d.warn.String()) strBuilder.WriteString(d.warn.String())
} }
strBuilder.WriteString(";") strBuilder.WriteRune(';')
if d.bits&PDatCrit != 0 { if d.bits&PDatCrit != 0 {
strBuilder.WriteString(d.crit.String()) strBuilder.WriteString(d.crit.String())
} }
strBuilder.WriteString(";") strBuilder.WriteRune(';')
if d.bits&PDatMin != 0 { if d.bits&PDatMin != 0 {
strBuilder.WriteString(d.min) strBuilder.WriteString(d.min)
} }
strBuilder.WriteString(";") strBuilder.WriteRune(';')
if d.bits&PDatMax != 0 { if d.bits&PDatMax != 0 {
strBuilder.WriteString(d.max) strBuilder.WriteString(d.max)
} }

View file

@ -1,108 +1,261 @@
package perfdata // import nocternity.net/gomonop/pkg/perfdata package perfdata
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func assert(t *testing.T, check bool, msg string) { func TestNewNoValue(t *testing.T) {
if !check { const label = "label"
t.Errorf(msg) 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) { func TestNewValidValue(t *testing.T) {
defer func() { const label = "label"
if r := recover(); r == nil { const units = UomNone
t.Errorf(msg) const value = "1234"
}
}() out := New(label, units, value)
f()
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) { func TestNewInvalidValue(t *testing.T) {
validValues := []string{ assert.Panics(t, func() { New("label", UomNone, "nope") })
"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 TestValueCheckInvalid(t *testing.T) { func TestSetWarn(t *testing.T) {
invalidValues := []string{".", "-.", "a", " ", ""} rangeValue := Range{start: "A", end: "B"}
rangeStr := rangeValue.String()
for _, value := range invalidValues { data := PerfData{}
if valueCheck.MatchString(value) { data.SetWarn(&rangeValue)
t.Errorf("'%s' is an invalid value string", value)
} assert.True(t, data.bits&PDatWarn != 0)
} assert.Equal(t, rangeStr, data.warn.String())
} }
func TestPdrMaxInvalid(t *testing.T) { func TestSetWarnTwice(t *testing.T) {
assertPanic( range1Value := Range{start: "A", end: "B"}
t, func() { PDRMax("") }, range2Value := Range{start: "C", end: "D"}
"Created PerfDataRange with invalid max value", 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) { func TestSetCrit(t *testing.T) {
value := "123" rangeValue := Range{start: "A", end: "B"}
pdr := PDRMax(value) rangeStr := rangeValue.String()
assert(t, pdr.start == "0", "Min value should be '0'")
assert(t, pdr.end == value, "Max value not copied to PerfDataRange") data := PerfData{}
assert(t, !pdr.inside, "Inside flag should not be set") data.SetCrit(&rangeValue)
assert.True(t, data.bits&PDatCrit != 0)
assert.Equal(t, rangeStr, data.crit.String())
} }
func TestPdrMinMaxInvalid(t *testing.T) { func TestSetCritTwice(t *testing.T) {
assertPanic( range1Value := Range{start: "A", end: "B"}
t, func() { PDRMinMax("", "123") }, range2Value := Range{start: "C", end: "D"}
"Created PerfDataRange with invalid min value", range2Str := range2Value.String()
) require.NotEqual(t, range2Str, range1Value.String())
assertPanic(
t, func() { PDRMinMax("123", "") }, data := PerfData{}
"Created PerfDataRange with invalid max value", 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) { func TestSetMin(t *testing.T) {
min, max := "123", "456" const min = "100"
pdr := PDRMinMax(min, max)
assert(t, pdr.start == min, "Min value not copied to PerfDataRange") data := PerfData{}
assert(t, pdr.end == max, "Max value not copied to PerfDataRange") data.SetMin(min)
assert(t, !pdr.inside, "Inside flag should not be set")
assert.True(t, data.bits&PDatMin != 0)
assert.Equal(t, min, data.min)
} }
func TestPdrInside(t *testing.T) { func TestSetMinInvalid(t *testing.T) {
pdr := &PDRange{} data := PerfData{}
pdr = pdr.Inside() assert.Panics(t, func() { data.SetMin("nope") })
assert(t, pdr.inside, "Inside flag should be set")
pdr = pdr.Inside()
assert(t, pdr.inside, "Inside flag should still be set")
} }
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 { type Test struct {
pdr PDRange PerfData
out string expected string
} }
range1 := Range{start: "A", end: "B"}
range2 := Range{start: "C", end: "D"}
tests := []Test{ tests := []Test{
{pdr: PDRange{start: "Y", end: "X"}, out: "Y:X"}, {
{pdr: PDRange{end: "X"}, out: "~:X"}, PerfData{
{pdr: PDRange{start: "0", end: "X"}, out: ":X"}, Label: "label",
{pdr: PDRange{inside: true, start: "Y", end: "X"}, out: "@Y:X"}, 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 { for _, test := range tests {
result := test.pdr.String() assert.Equal(t, test.expected, test.PerfData.String())
assert(
t,
result == test.out,
fmt.Sprintf("Expected '%s', got '%s'", test.out, result),
)
} }
} }

62
pkg/perfdata/range.go Normal file
View file

@ -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
}

View file

@ -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)
}
}

20
pkg/perfdata/units.go Normal file
View file

@ -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]
}

View file

@ -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)
}
}

View file

@ -1,122 +1,20 @@
// Package plugin implements a helper that can be used to implement a Nagios, package plugin // import nocternity.net/gomonop/pkg/plugin
// Centreon, Icinga... service monitoring plugin.
package plugin // import nocternity.net/gomonop/pkg/perfdata
import ( import "nocternity.net/gomonop/pkg/results"
"container/list"
"fmt"
"os"
"strings"
"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 // CheckArguments ensures that the arguments that were passed to the plugin
// corresponding integer value will be used as the program's exit code, // actually make sense. Errors should be stored in the plugin's results.
// to be interpreted by the monitoring system. CheckArguments() bool
type Status int
// Plugin exit statuses. // RunCheck actually runs whatever checks are implemented by the plugin and
const ( // updates the results accordingly.
OK Status = iota RunCheck()
WARNING
CRITICAL
UNKNOWN
)
// String representations of the plugin statuses.
func (s Status) String() string {
return [...]string{"OK", "WARNING", "ERROR", "UNKNOWN"}[s]
} }
// Plugin represents the monitoring plugin's state, including its name, // Builder is a function that can be called in order to instantiate a plugin.
// return status and message, additional lines of text, and performance type Builder func() Plugin
// 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))
}

View file

@ -1,9 +0,0 @@
package program // import nocternity.net/gomonop/pkg/program
type Program interface {
CheckArguments() bool
RunCheck()
Done()
}
type Builder func() Program

107
pkg/results/results.go Normal file
View file

@ -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)
}

175
pkg/results/results_test.go Normal file
View file

@ -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())
}

19
pkg/results/status.go Normal file
View file

@ -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]
}

View file

@ -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))
}

View file

@ -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)
}
}