From 0776ee6741d98383915afea2e5ddc6f8dcd700f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:12:55 +0200 Subject: [PATCH 01/12] chore: add `testify` to dependencies --- go.mod | 4 ++++ go.sum | 9 +++++++++ 2 files changed, 13 insertions(+) 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= -- 2.39.5 From 70670e0657008e4dd427e52c144b30c805d649c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:14:59 +0200 Subject: [PATCH 02/12] refactor(pkg): split `perfdata` across multiple files --- pkg/perfdata/internals.go | 25 ++++++++++ pkg/perfdata/perfdata.go | 101 -------------------------------------- pkg/perfdata/range.go | 62 +++++++++++++++++++++++ pkg/perfdata/units.go | 20 ++++++++ 4 files changed, 107 insertions(+), 101 deletions(-) create mode 100644 pkg/perfdata/internals.go create mode 100644 pkg/perfdata/range.go create mode 100644 pkg/perfdata/units.go 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/perfdata.go b/pkg/perfdata/perfdata.go index bc7195b..b1943f3 100644 --- a/pkg/perfdata/perfdata.go +++ b/pkg/perfdata/perfdata.go @@ -4,110 +4,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 { diff --git a/pkg/perfdata/range.go b/pkg/perfdata/range.go new file mode 100644 index 0000000..0c7a3a4 --- /dev/null +++ b/pkg/perfdata/range.go @@ -0,0 +1,62 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +// 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 +} 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] +} -- 2.39.5 From db47981d23582a758325aae1e1841146f2996cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:15:40 +0200 Subject: [PATCH 03/12] test(pkg): add tests for all parts of `perfdata` --- pkg/perfdata/internals_test.go | 50 ++++++ pkg/perfdata/perfdata_test.go | 303 +++++++++++++++++++++++++-------- pkg/perfdata/range_test.go | 67 ++++++++ pkg/perfdata/units_test.go | 26 +++ 4 files changed, 371 insertions(+), 75 deletions(-) create mode 100644 pkg/perfdata/internals_test.go create mode 100644 pkg/perfdata/range_test.go create mode 100644 pkg/perfdata/units_test.go 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_test.go b/pkg/perfdata/perfdata_test.go index 0c0fe2f..1773bc6 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 := PDRange{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 := PDRange{start: "A", end: "B"} + range2Value := PDRange{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 := PDRange{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 := PDRange{start: "A", end: "B"} + range2Value := PDRange{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 := PDRange{start: "A", end: "B"} + range2 := PDRange{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_test.go b/pkg/perfdata/range_test.go new file mode 100644 index 0000000..4eb182b --- /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() { PDRMax("") }, + "Created PerfDataRange with invalid max value", + ) +} + +func TestRangeMax(t *testing.T) { + value := "123" + pdr := PDRMax(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() { PDRMinMax("", "123") }, + "Created PerfDataRange with invalid min value", + ) + assert.Panics( + t, func() { PDRMinMax("123", "") }, + "Created PerfDataRange with invalid max value", + ) +} + +func TestRangeMinMax(t *testing.T) { + min, max := "123", "456" + pdr := PDRMinMax(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 := &PDRange{} + 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 PDRange + out string + } + 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"}, + } + + 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_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) + } +} -- 2.39.5 From 47b342b3179dd05a165422a77d7408826a8ddcb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:16:57 +0200 Subject: [PATCH 04/12] refactor(pkg): rename `PDRange` to `Range` --- cmd/sslcert/main.go | 4 ++-- pkg/perfdata/perfdata.go | 6 +++--- pkg/perfdata/perfdata_test.go | 16 ++++++++-------- pkg/perfdata/range.go | 14 +++++++------- pkg/perfdata/range_test.go | 22 +++++++++++----------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/cmd/sslcert/main.go b/cmd/sslcert/main.go index d942cc3..facd646 100644 --- a/cmd/sslcert/main.go +++ b/cmd/sslcert/main.go @@ -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) } diff --git a/pkg/perfdata/perfdata.go b/pkg/perfdata/perfdata.go index b1943f3..bc4ba52 100644 --- a/pkg/perfdata/perfdata.go +++ b/pkg/perfdata/perfdata.go @@ -14,7 +14,7 @@ type PerfData struct { units UnitOfMeasurement bits perfDataBits value string - warn, crit PDRange + warn, crit Range min, max string } @@ -35,13 +35,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 } diff --git a/pkg/perfdata/perfdata_test.go b/pkg/perfdata/perfdata_test.go index 1773bc6..894077c 100644 --- a/pkg/perfdata/perfdata_test.go +++ b/pkg/perfdata/perfdata_test.go @@ -40,7 +40,7 @@ func TestNewInvalidValue(t *testing.T) { } func TestSetWarn(t *testing.T) { - rangeValue := PDRange{start: "A", end: "B"} + rangeValue := Range{start: "A", end: "B"} rangeStr := rangeValue.String() data := PerfData{} @@ -51,8 +51,8 @@ func TestSetWarn(t *testing.T) { } func TestSetWarnTwice(t *testing.T) { - range1Value := PDRange{start: "A", end: "B"} - range2Value := PDRange{start: "C", end: "D"} + range1Value := Range{start: "A", end: "B"} + range2Value := Range{start: "C", end: "D"} range2Str := range2Value.String() require.NotEqual(t, range2Str, range1Value.String()) @@ -65,7 +65,7 @@ func TestSetWarnTwice(t *testing.T) { } func TestSetCrit(t *testing.T) { - rangeValue := PDRange{start: "A", end: "B"} + rangeValue := Range{start: "A", end: "B"} rangeStr := rangeValue.String() data := PerfData{} @@ -76,8 +76,8 @@ func TestSetCrit(t *testing.T) { } func TestSetCritTwice(t *testing.T) { - range1Value := PDRange{start: "A", end: "B"} - range2Value := PDRange{start: "C", end: "D"} + range1Value := Range{start: "A", end: "B"} + range2Value := Range{start: "C", end: "D"} range2Str := range2Value.String() require.NotEqual(t, range2Str, range1Value.String()) @@ -141,8 +141,8 @@ func TestString(t *testing.T) { expected string } - range1 := PDRange{start: "A", end: "B"} - range2 := PDRange{start: "C", end: "D"} + range1 := Range{start: "A", end: "B"} + range2 := Range{start: "C", end: "D"} tests := []Test{ { diff --git a/pkg/perfdata/range.go b/pkg/perfdata/range.go index 0c7a3a4..e4ad2c7 100644 --- a/pkg/perfdata/range.go +++ b/pkg/perfdata/range.go @@ -1,7 +1,7 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata // Performance data range. -type PDRange struct { +type Range struct { start string end string inside bool @@ -9,11 +9,11 @@ type PDRange struct { // Creates a performance data range from -inf to 0 and from the specified // value to +inf. -func PDRMax(max string) *PDRange { +func RangeMax(max string) *Range { if !valueCheck.MatchString(max) { panic("invalid performance data range maximum value") } - pdRange := &PDRange{} + pdRange := &Range{} pdRange.start = "0" pdRange.end = max return pdRange @@ -21,28 +21,28 @@ func PDRMax(max string) *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 { +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 := &PDRange{} + pdRange := &Range{} pdRange.start = min pdRange.end = max return pdRange } // Inverts the range. -func (r *PDRange) Inside() *PDRange { +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 *PDRange) String() string { +func (r *Range) String() string { var start, inside string switch r.start { diff --git a/pkg/perfdata/range_test.go b/pkg/perfdata/range_test.go index 4eb182b..26d1f21 100644 --- a/pkg/perfdata/range_test.go +++ b/pkg/perfdata/range_test.go @@ -8,14 +8,14 @@ import ( func TestRangeMaxInvalid(t *testing.T) { assert.Panics( - t, func() { PDRMax("") }, + t, func() { RangeMax("") }, "Created PerfDataRange with invalid max value", ) } func TestRangeMax(t *testing.T) { value := "123" - pdr := PDRMax(value) + 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") @@ -23,25 +23,25 @@ func TestRangeMax(t *testing.T) { func TestRangeMinMaxInvalid(t *testing.T) { assert.Panics( - t, func() { PDRMinMax("", "123") }, + t, func() { RangeMinMax("", "123") }, "Created PerfDataRange with invalid min value", ) assert.Panics( - t, func() { PDRMinMax("123", "") }, + t, func() { RangeMinMax("123", "") }, "Created PerfDataRange with invalid max value", ) } func TestRangeMinMax(t *testing.T) { min, max := "123", "456" - pdr := PDRMinMax(min, max) + 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 := &PDRange{} + pdr := &Range{} pdr = pdr.Inside() assert.True(t, pdr.inside, "Inside flag should be set") pdr = pdr.Inside() @@ -50,14 +50,14 @@ func TestRangeInside(t *testing.T) { func TestRangeString(t *testing.T) { type Test struct { - pdr PDRange + pdr Range out string } 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"}, + {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 { -- 2.39.5 From cf88d63bc27094240cf07757e89bd79c988ada4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:20:41 +0200 Subject: [PATCH 05/12] refactor(pkg): clean up perfdata to string conversion --- pkg/perfdata/perfdata.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/perfdata/perfdata.go b/pkg/perfdata/perfdata.go index bc4ba52..fc1914e 100644 --- a/pkg/perfdata/perfdata.go +++ b/pkg/perfdata/perfdata.go @@ -3,7 +3,6 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( - "fmt" "strings" ) @@ -70,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) } -- 2.39.5 From ebdb99be8b616c137398cbf66d7e654eeded4597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:24:19 +0200 Subject: [PATCH 06/12] refactor(pkg): split `plugin` into multiple files --- pkg/plugin/plugin.go | 18 ------------------ pkg/plugin/status.go | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 pkg/plugin/status.go diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index f32e360..c78a196 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -11,24 +11,6 @@ import ( "nocternity.net/gomonop/pkg/perfdata" ) -// 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 ( - OK Status = iota - 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, // return status and message, additional lines of text, and performance // data to be encoded in the output. diff --git a/pkg/plugin/status.go b/pkg/plugin/status.go new file mode 100644 index 0000000..b7db5a8 --- /dev/null +++ b/pkg/plugin/status.go @@ -0,0 +1,19 @@ +package plugin // import nocternity.net/gomonop/pkg/perfdata + +// 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 ( + OK Status = iota + WARNING + CRITICAL + UNKNOWN +) + +// String representations of the plugin statuses. +func (s Status) String() string { + return [...]string{"OK", "WARNING", "ERROR", "UNKNOWN"}[s] +} -- 2.39.5 From b3aa7dfcad0ad39fc19b1f733948f04715437248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:24:52 +0200 Subject: [PATCH 07/12] refactor(pkg): rename plugin status constants --- cmd/sslcert/main.go | 24 ++++++++++++------------ cmd/zoneserial/main.go | 18 +++++++++--------- pkg/plugin/plugin.go | 2 +- pkg/plugin/status.go | 8 ++++---- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cmd/sslcert/main.go b/cmd/sslcert/main.go index facd646..efb4971 100644 --- a/cmd/sslcert/main.go +++ b/cmd/sslcert/main.go @@ -224,20 +224,20 @@ func (program *checkProgram) Done() { // if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no hostname specified") + program.plugin.SetState(plugin.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(plugin.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(plugin.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(plugin.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(plugin.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(plugin.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(plugin.StatusCritical, "names missing from SAN domain names") } return certificateIsOk } @@ -308,7 +308,7 @@ func (program *checkProgram) checkNames() bool { // values. func (program *checkProgram) checkCertificateExpiry(tlDays int) (plugin.Status, string) { if tlDays <= 0 { - return plugin.CRITICAL, "certificate expired" + return plugin.StatusCritical, "certificate expired" } var limitStr string @@ -317,15 +317,15 @@ func (program *checkProgram) checkCertificateExpiry(tlDays int) (plugin.Status, switch { case program.crit > 0 && tlDays <= program.crit: limitStr = fmt.Sprintf(" (<= %d)", program.crit) - state = plugin.CRITICAL + state = plugin.StatusCritical case program.warn > 0 && tlDays <= program.warn: limitStr = fmt.Sprintf(" (<= %d)", program.warn) - state = plugin.WARNING + state = plugin.StatusWarning default: limitStr = "" - state = plugin.OK + state = plugin.StatusOK } statusString := fmt.Sprintf("certificate will expire in %d days%s", @@ -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(plugin.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..e8bc7d6 100644 --- a/cmd/zoneserial/main.go +++ b/cmd/zoneserial/main.go @@ -88,7 +88,7 @@ func NewProgram() program.Program { // Terminate the monitoring check program. func (program *checkProgram) Done() { if r := recover(); r != nil { - program.plugin.SetState(plugin.UNKNOWN, "Internal error") + program.plugin.SetState(plugin.StatusUnknown, "Internal error") program.plugin.AddLinef("Error info: %v", r) } program.plugin.Done() @@ -97,23 +97,23 @@ func (program *checkProgram) Done() { // 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(plugin.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(plugin.StatusUnknown, "invalid DNS port number") return false } if program.zone == "" { - program.plugin.SetState(plugin.UNKNOWN, "no DNS zone specified") + program.plugin.SetState(plugin.StatusUnknown, "no DNS zone specified") return false } if program.rsHostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no reference DNS hostname specified") + program.plugin.SetState(plugin.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(plugin.StatusUnknown, "invalid reference DNS port number") return false } program.hostname = strings.ToLower(program.hostname) @@ -180,12 +180,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(plugin.StatusUnknown, "could not read serials") return } if cSerial == rSerial { - program.plugin.SetState(plugin.OK, "serials match") + program.plugin.SetState(plugin.StatusOK, "serials match") } else { - program.plugin.SetState(plugin.CRITICAL, "serials mismatch") + program.plugin.SetState(plugin.StatusCritical, "serials mismatch") } } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index c78a196..061b06f 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -26,7 +26,7 @@ type Plugin struct { func New(name string) *Plugin { p := new(Plugin) p.name = name - p.status = UNKNOWN + p.status = StatusUnknown p.message = "no status set" p.perfData = make(map[string]*perfdata.PerfData) return p diff --git a/pkg/plugin/status.go b/pkg/plugin/status.go index b7db5a8..170f594 100644 --- a/pkg/plugin/status.go +++ b/pkg/plugin/status.go @@ -7,10 +7,10 @@ type Status int // Plugin exit statuses. const ( - OK Status = iota - WARNING - CRITICAL - UNKNOWN + StatusOK Status = iota + StatusWarning + StatusCritical + StatusUnknown ) // String representations of the plugin statuses. -- 2.39.5 From 5432360f0e5ecb4368a946d84e54826afc5aa90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:46:33 +0200 Subject: [PATCH 08/12] refactor(pkg): refactor `Plugin` to make it easier to test Writing the plugin's output string and exiting the program will no longer take place in the plugin status itself. It will only be done in the main program. --- cmd/sslcert/main.go | 6 +++--- cmd/zoneserial/main.go | 10 +++------- main.go | 13 ++++++++++++- pkg/plugin/plugin.go | 16 +++++++++------- pkg/program/program.go | 4 +++- 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/cmd/sslcert/main.go b/cmd/sslcert/main.go index efb4971..be9c921 100644 --- a/cmd/sslcert/main.go +++ b/cmd/sslcert/main.go @@ -215,9 +215,9 @@ func NewProgram() program.Program { return program } -// Terminate the monitoring check program. -func (program *checkProgram) Done() { - program.plugin.Done() +// Return the program's output value. +func (program *checkProgram) Output() *plugin.Plugin { + return program.plugin } // Check the values that were specified from the command line. Returns true diff --git a/cmd/zoneserial/main.go b/cmd/zoneserial/main.go index e8bc7d6..69bd27d 100644 --- a/cmd/zoneserial/main.go +++ b/cmd/zoneserial/main.go @@ -85,13 +85,9 @@ func NewProgram() program.Program { return program } -// Terminate the monitoring check program. -func (program *checkProgram) Done() { - if r := recover(); r != nil { - program.plugin.SetState(plugin.StatusUnknown, "Internal error") - program.plugin.AddLinef("Error info: %v", r) - } - program.plugin.Done() +// Return the program's output value. +func (program *checkProgram) Output() *plugin.Plugin { + return program.plugin } // Check the values that were specified from the command line. Returns true if the arguments made sense. diff --git a/main.go b/main.go index e8ce79a..af989b1 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "nocternity.net/gomonop/cmd/sslcert" "nocternity.net/gomonop/cmd/zoneserial" + "nocternity.net/gomonop/pkg/plugin" "nocternity.net/gomonop/pkg/program" "nocternity.net/gomonop/pkg/version" ) @@ -55,7 +56,17 @@ func getProgram() program.Program { func main() { program := getProgram() - defer program.Done() + + output := program.Output() + defer func() { + if r := recover(); r != nil { + output.SetState(plugin.StatusUnknown, "Internal error") + output.AddLinef("Error info: %v", r) + } + fmt.Println(output.String()) + os.Exit(output.ExitCode()) + }() + if program.CheckArguments() { program.RunCheck() } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 061b06f..b967e9f 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -5,7 +5,6 @@ package plugin // import nocternity.net/gomonop/pkg/perfdata import ( "container/list" "fmt" - "os" "strings" "nocternity.net/gomonop/pkg/perfdata" @@ -70,10 +69,9 @@ func (p *Plugin) AddPerfData(pd *perfdata.PerfData) { 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() { +// String generates the plugin's text output from its name, status, text data +// and performance data. +func (p *Plugin) String() string { var strBuilder strings.Builder strBuilder.WriteString(p.name) strBuilder.WriteString(" ") @@ -99,6 +97,10 @@ func (p *Plugin) Done() { strBuilder.WriteString(em.Value.(string)) } } - fmt.Println(strBuilder.String()) - os.Exit(int(p.status)) + return strBuilder.String() +} + +// ExitCode returns the plugin's exit code. +func (p *Plugin) ExitCode() int { + return int(p.status) } diff --git a/pkg/program/program.go b/pkg/program/program.go index d8b3510..2d5a1dd 100644 --- a/pkg/program/program.go +++ b/pkg/program/program.go @@ -1,9 +1,11 @@ package program // import nocternity.net/gomonop/pkg/program +import "nocternity.net/gomonop/pkg/plugin" + type Program interface { + Output() *plugin.Plugin CheckArguments() bool RunCheck() - Done() } type Builder func() Program -- 2.39.5 From 1e0304f45036181e7a801080dcf92f001614e846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Fri, 19 Jul 2024 23:50:50 +0200 Subject: [PATCH 09/12] chore(pkg): fix copypasta in comments --- pkg/plugin/plugin.go | 2 +- pkg/plugin/status.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index b967e9f..b7db756 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -1,6 +1,6 @@ // 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" diff --git a/pkg/plugin/status.go b/pkg/plugin/status.go index 170f594..71558ca 100644 --- a/pkg/plugin/status.go +++ b/pkg/plugin/status.go @@ -1,4 +1,4 @@ -package plugin // import nocternity.net/gomonop/pkg/perfdata +package plugin // import nocternity.net/gomonop/pkg/plugin // Status represents the return status of the monitoring plugin. The // corresponding integer value will be used as the program's exit code, -- 2.39.5 From ffbec78937c649d2ccc3cb46f8044c95dc3aaf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Sat, 20 Jul 2024 00:05:58 +0200 Subject: [PATCH 10/12] test(pkg): add tests for `plugin` --- pkg/plugin/plugin_test.go | 175 ++++++++++++++++++++++++++++++++++++++ pkg/plugin/status_test.go | 26 ++++++ 2 files changed, 201 insertions(+) create mode 100644 pkg/plugin/plugin_test.go create mode 100644 pkg/plugin/status_test.go diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go new file mode 100644 index 0000000..96d4bdc --- /dev/null +++ b/pkg/plugin/plugin_test.go @@ -0,0 +1,175 @@ +package plugin // import nocternity.net/gomonop/pkg/plugin + +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 := Plugin{} + + p.SetState(StatusWarning, "test") + + assert.Equal(t, p.status, StatusWarning) + assert.Equal(t, p.message, "test") +} + +func TestAddLineFirst(t *testing.T) { + p := Plugin{} + + 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 := Plugin{} + 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 := Plugin{} + + 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 := Plugin{} + + 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 := Plugin{} + 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 := Plugin{} + 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 { + Plugin + expected string + } + + pdat := perfdata.PerfData{Label: "test"} + tests := []Test{ + { + Plugin{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + }, + "test WARNING: test", + }, + { + func() Plugin { + p := Plugin{ + 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() Plugin { + p := Plugin{ + 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() Plugin { + p := Plugin{ + 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.Plugin.String() + assert.Equal(t, test.expected, result, "Expected '%s', got '%s'", test.expected, result) + } +} + +func TestExitCode(t *testing.T) { + p := Plugin{} + + 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/plugin/status_test.go b/pkg/plugin/status_test.go new file mode 100644 index 0000000..1c8fc7e --- /dev/null +++ b/pkg/plugin/status_test.go @@ -0,0 +1,26 @@ +package plugin // import nocternity.net/gomonop/pkg/plugin + +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)) +} -- 2.39.5 From 1a29325c341332d55ccd2da71b47afafdf5e581d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Sat, 20 Jul 2024 00:20:28 +0200 Subject: [PATCH 11/12] refactor(pkg): rename internals so their names actually make sense A "program" was in fact a plugin, while a "plugin" represented the plugin's results. --- cmd/sslcert/main.go | 38 +++--- cmd/zoneserial/main.go | 28 ++--- main.go | 30 ++--- pkg/plugin/plugin.go | 114 +++--------------- pkg/program/program.go | 11 -- pkg/results/results.go | 107 ++++++++++++++++ .../results_test.go} | 36 +++--- pkg/{plugin => results}/status.go | 2 +- pkg/{plugin => results}/status_test.go | 2 +- 9 files changed, 189 insertions(+), 179 deletions(-) delete mode 100644 pkg/program/program.go create mode 100644 pkg/results/results.go rename pkg/{plugin/plugin_test.go => results/results_test.go} (90%) rename pkg/{plugin => results}/status.go (88%) rename pkg/{plugin => results}/status_test.go (90%) diff --git a/cmd/sslcert/main.go b/cmd/sslcert/main.go index be9c921..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,16 +207,16 @@ 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 } // Return the program's output value. -func (program *checkProgram) Output() *plugin.Plugin { +func (program *checkProgram) Results() *results.Results { return program.plugin } @@ -224,20 +224,20 @@ func (program *checkProgram) Output() *plugin.Plugin { // if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(plugin.StatusUnknown, "no hostname specified") + program.plugin.SetState(results.StatusUnknown, "no hostname specified") return false } if program.port < 1 || program.port > 65535 { - program.plugin.SetState(plugin.StatusUnknown, "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.StatusUnknown, "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.StatusUnknown, 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.StatusWarning, + 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.StatusCritical, "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.StatusCritical, "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.StatusCritical, "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.StatusCritical + state = results.StatusCritical case program.warn > 0 && tlDays <= program.warn: limitStr = fmt.Sprintf(" (<= %d)", program.warn) - state = plugin.StatusWarning + state = results.StatusWarning default: limitStr = "" - state = plugin.StatusOK + state = results.StatusOK } statusString := fmt.Sprintf("certificate will expire in %d days%s", @@ -351,7 +351,7 @@ func (program *checkProgram) setPerfData(tlDays int) { func (program *checkProgram) RunCheck() { err := program.getCertificate() if err != nil { - program.plugin.SetState(plugin.StatusUnknown, 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 69bd27d..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,39 +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 } // Return the program's output value. -func (program *checkProgram) Output() *plugin.Plugin { +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.StatusUnknown, "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.StatusUnknown, "invalid DNS port number") + program.plugin.SetState(results.StatusUnknown, "invalid DNS port number") return false } if program.zone == "" { - program.plugin.SetState(plugin.StatusUnknown, "no DNS zone specified") + program.plugin.SetState(results.StatusUnknown, "no DNS zone specified") return false } if program.rsHostname == "" { - program.plugin.SetState(plugin.StatusUnknown, "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.StatusUnknown, "invalid reference DNS port number") + program.plugin.SetState(results.StatusUnknown, "invalid reference DNS port number") return false } program.hostname = strings.ToLower(program.hostname) @@ -176,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.StatusUnknown, "could not read serials") + program.plugin.SetState(results.StatusUnknown, "could not read serials") return } if cSerial == rSerial { - program.plugin.SetState(plugin.StatusOK, "serials match") + program.plugin.SetState(results.StatusOK, "serials match") } else { - program.plugin.SetState(plugin.StatusCritical, "serials mismatch") + program.plugin.SetState(results.StatusCritical, "serials mismatch") } } diff --git a/main.go b/main.go index af989b1..fd15700 100644 --- a/main.go +++ b/main.go @@ -8,33 +8,33 @@ import ( "nocternity.net/gomonop/cmd/sslcert" "nocternity.net/gomonop/cmd/zoneserial" "nocternity.net/gomonop/pkg/plugin" - "nocternity.net/gomonop/pkg/program" + "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) @@ -44,30 +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() + runPlugin := getPlugin() - output := program.Output() + output := runPlugin.Results() defer func() { if r := recover(); r != nil { - output.SetState(plugin.StatusUnknown, "Internal error") + output.SetState(results.StatusUnknown, "Internal error") output.AddLinef("Error info: %v", r) } fmt.Println(output.String()) os.Exit(output.ExitCode()) }() - if program.CheckArguments() { - program.RunCheck() + if runPlugin.CheckArguments() { + runPlugin.RunCheck() } } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index b7db756..a60366f 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -1,106 +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/plugin -import ( - "container/list" - "fmt" - "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 -// 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 + // 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 + + // RunCheck actually runs whatever checks are implemented by the plugin and + // updates the results accordingly. + RunCheck() } -// 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 = 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 *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 -} - -// String generates the plugin's text output from its name, status, text data -// and performance data. -func (p *Plugin) 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 *Plugin) ExitCode() int { - return 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 2d5a1dd..0000000 --- a/pkg/program/program.go +++ /dev/null @@ -1,11 +0,0 @@ -package program // import nocternity.net/gomonop/pkg/program - -import "nocternity.net/gomonop/pkg/plugin" - -type Program interface { - Output() *plugin.Plugin - CheckArguments() bool - RunCheck() -} - -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/plugin/plugin_test.go b/pkg/results/results_test.go similarity index 90% rename from pkg/plugin/plugin_test.go rename to pkg/results/results_test.go index 96d4bdc..32ddacf 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/results/results_test.go @@ -1,4 +1,4 @@ -package plugin // import nocternity.net/gomonop/pkg/plugin +package results // import nocternity.net/gomonop/pkg/results import ( "container/list" @@ -19,7 +19,7 @@ func TestNew(t *testing.T) { } func TestSetState(t *testing.T) { - p := Plugin{} + p := Results{} p.SetState(StatusWarning, "test") @@ -28,7 +28,7 @@ func TestSetState(t *testing.T) { } func TestAddLineFirst(t *testing.T) { - p := Plugin{} + p := Results{} p.AddLine("test") @@ -38,7 +38,7 @@ func TestAddLineFirst(t *testing.T) { } func TestAddLineNext(t *testing.T) { - p := Plugin{} + p := Results{} p.extraText = list.New() p.AddLine("test") @@ -48,7 +48,7 @@ func TestAddLineNext(t *testing.T) { } func TestAddLinef(t *testing.T) { - p := Plugin{} + p := Results{} p.AddLinef("test %d", 123) @@ -57,7 +57,7 @@ func TestAddLinef(t *testing.T) { } func TestAddLines(t *testing.T) { - p := Plugin{} + p := Results{} p.AddLines([]string{"test", "test2"}) @@ -67,7 +67,7 @@ func TestAddLines(t *testing.T) { } func TestAddPerfData(t *testing.T) { - p := Plugin{} + p := Results{} p.perfData = make(map[string]*perfdata.PerfData) p.AddPerfData(&perfdata.PerfData{Label: "test"}) @@ -78,7 +78,7 @@ func TestAddPerfData(t *testing.T) { } func TestAddPerfDataDuplicate(t *testing.T) { - p := Plugin{} + p := Results{} p.perfData = make(map[string]*perfdata.PerfData) p.perfData["test"] = &perfdata.PerfData{Label: "test"} @@ -87,14 +87,14 @@ func TestAddPerfDataDuplicate(t *testing.T) { func TestString(t *testing.T) { type Test struct { - Plugin + Results expected string } pdat := perfdata.PerfData{Label: "test"} tests := []Test{ { - Plugin{ + Results{ name: "test", status: StatusWarning, message: "test", @@ -103,8 +103,8 @@ func TestString(t *testing.T) { "test WARNING: test", }, { - func() Plugin { - p := Plugin{ + func() Results { + p := Results{ name: "test", status: StatusWarning, message: "test", @@ -118,8 +118,8 @@ func TestString(t *testing.T) { "test WARNING: test\ntest 1\ntest 2", }, { - func() Plugin { - p := Plugin{ + func() Results { + p := Results{ name: "test", status: StatusWarning, message: "test", @@ -133,8 +133,8 @@ func TestString(t *testing.T) { pdat.String(), }, { - func() Plugin { - p := Plugin{ + func() Results { + p := Results{ name: "test", status: StatusWarning, message: "test", @@ -153,13 +153,13 @@ func TestString(t *testing.T) { } for _, test := range tests { - result := test.Plugin.String() + result := test.Results.String() assert.Equal(t, test.expected, result, "Expected '%s', got '%s'", test.expected, result) } } func TestExitCode(t *testing.T) { - p := Plugin{} + p := Results{} p.status = StatusOK assert.Equal(t, int(StatusOK), p.ExitCode()) diff --git a/pkg/plugin/status.go b/pkg/results/status.go similarity index 88% rename from pkg/plugin/status.go rename to pkg/results/status.go index 71558ca..6238a20 100644 --- a/pkg/plugin/status.go +++ b/pkg/results/status.go @@ -1,4 +1,4 @@ -package plugin // import nocternity.net/gomonop/pkg/plugin +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, diff --git a/pkg/plugin/status_test.go b/pkg/results/status_test.go similarity index 90% rename from pkg/plugin/status_test.go rename to pkg/results/status_test.go index 1c8fc7e..8f81f43 100644 --- a/pkg/plugin/status_test.go +++ b/pkg/results/status_test.go @@ -1,4 +1,4 @@ -package plugin // import nocternity.net/gomonop/pkg/plugin +package results // import nocternity.net/gomonop/pkg/results import ( "testing" -- 2.39.5 From 9f282c40f90386735c9dc52056758f0a8f93eff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Sat, 20 Jul 2024 00:25:31 +0200 Subject: [PATCH 12/12] test(pkg): add tests for `version` --- pkg/version/version_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 pkg/version/version_test.go 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) + } +} -- 2.39.5