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] 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"