From 8b8bd63ddce19980f847dbc33862c3afe6904f85 Mon Sep 17 00:00:00 2001 From: Alvar Penning Date: Mon, 15 Jul 2024 14:36:10 +0200 Subject: [PATCH] Channel default value by changing default logic In a nutshell, the newly introduced plugin.PopulateDefaults function populates all fields of a Plugin-implementing struct with those fields from Info.ConfigAttributes where ConfigOption.Default is set. Thus, a single function call before parsing the user-submitted configuration within the Plugin.SetConfig method sets default values. As a result of the discussion between the Go daemon team and the web team, summarized in #205, web does not store or stores default values as JSON "null" values. The Go JSON unmarshaller does not overwrite existing field values for JSON nulls. Prior, an already JSON-encoded version of the ConfigOption slice was present in plugin.Info. Thus, this data wasn't easily available anymore. As the new code now needs to access this field, it was changed and a custom sql driver.Valuer was implemented for a slice type. While working on this, all ConfigOptions were put in the same order as the struct fields. Closes #205. --- cmd/channels/email/main.go | 49 +++++++++++----------- cmd/channels/email/main_test.go | 72 +++++++++++++++++++++++++++++++++ cmd/channels/rocketchat/main.go | 13 +++--- cmd/channels/webhook/main.go | 16 ++++---- pkg/plugin/plugin.go | 40 +++++++++++++++--- 5 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 cmd/channels/email/main_test.go diff --git a/cmd/channels/email/main.go b/cmd/channels/email/main.go index 5119d535b..87580f5b0 100644 --- a/cmd/channels/email/main.go +++ b/cmd/channels/email/main.go @@ -95,7 +95,12 @@ func (ch *Email) Send(reversePath string, recipients []string, msg []byte) error } func (ch *Email) SetConfig(jsonStr json.RawMessage) error { - err := json.Unmarshal(jsonStr, ch) + err := plugin.PopulateDefaults(ch) + if err != nil { + return err + } + + err = json.Unmarshal(jsonStr, ch) if err != nil { return fmt.Errorf("failed to load config: %s %w", jsonStr, err) } @@ -108,24 +113,7 @@ func (ch *Email) SetConfig(jsonStr json.RawMessage) error { } func (ch *Email) GetInfo() *plugin.Info { - elements := []*plugin.ConfigOption{ - { - Name: "sender_name", - Type: "string", - Label: map[string]string{ - "en_US": "Sender Name", - "de_DE": "Absendername", - }, - }, - { - Name: "sender_mail", - Type: "string", - Label: map[string]string{ - "en_US": "Sender Address", - "de_DE": "Absenderadresse", - }, - Default: "icinga@example.com", - }, + configAttrs := plugin.ConfigOptions{ { Name: "host", Type: "string", @@ -146,6 +134,24 @@ func (ch *Email) GetInfo() *plugin.Info { Min: types.Int{NullInt64: sql.NullInt64{Int64: 1, Valid: true}}, Max: types.Int{NullInt64: sql.NullInt64{Int64: 65535, Valid: true}}, }, + { + Name: "sender_name", + Type: "string", + Label: map[string]string{ + "en_US": "Sender Name", + "de_DE": "Absendername", + }, + Default: "Icinga", + }, + { + Name: "sender_mail", + Type: "string", + Label: map[string]string{ + "en_US": "Sender Address", + "de_DE": "Absenderadresse", + }, + Default: "icinga@example.com", + }, { Name: "user", Type: "string", @@ -178,11 +184,6 @@ func (ch *Email) GetInfo() *plugin.Info { }, } - configAttrs, err := json.Marshal(elements) - if err != nil { - panic(err) - } - return &plugin.Info{ Name: "Email", Version: internal.Version.Version, diff --git a/cmd/channels/email/main_test.go b/cmd/channels/email/main_test.go new file mode 100644 index 000000000..d971c3bd0 --- /dev/null +++ b/cmd/channels/email/main_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestEmail_SetConfig(t *testing.T) { + tests := []struct { + name string + jsonMsg string + want *Email + wantErr bool + }{ + { + name: "empty-string", + jsonMsg: ``, + wantErr: true, + }, + { + name: "empty-json-obj-use-defaults", + jsonMsg: `{}`, + want: &Email{SenderName: "Icinga", SenderMail: "icinga@example.com"}, + }, + { + name: "sender-mail-overwrite", + jsonMsg: `{"sender_mail": "foo@bar"}`, + want: &Email{SenderName: "Icinga", SenderMail: "foo@bar"}, + }, + { + name: "sender-mail-overwrite-empty", + jsonMsg: `{"sender_mail": ""}`, + want: &Email{SenderName: "Icinga", SenderMail: ""}, + }, + { + name: "sender-mail-null", + jsonMsg: `{"sender_mail": null}`, + want: &Email{SenderName: "Icinga", SenderMail: ""}, + }, + { + name: "full-example-config", + jsonMsg: `{"sender_name":"icinga","sender_mail":"icinga@example.com","host":"smtp.example.com","port":"25","encryption":"none"}`, + want: &Email{ + Host: "smtp.example.com", + Port: "25", + SenderName: "icinga", + SenderMail: "icinga@example.com", + User: "", + Password: "", + Encryption: "none", + }, + }, + { + name: "user-but-missing-pass", + jsonMsg: `{"user": "foo"}`, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + email := &Email{} + err := email.SetConfig(json.RawMessage(tt.jsonMsg)) + assert.Equal(t, tt.wantErr, err != nil, "SetConfig() error = %v, wantErr = %t", err, tt.wantErr) + if err != nil { + return + } + + assert.Equal(t, tt.want, email, "Email differs") + }) + } +} diff --git a/cmd/channels/rocketchat/main.go b/cmd/channels/rocketchat/main.go index 143c5b9e0..b441234d5 100644 --- a/cmd/channels/rocketchat/main.go +++ b/cmd/channels/rocketchat/main.go @@ -77,12 +77,16 @@ func (ch *RocketChat) SendNotification(req *plugin.NotificationRequest) error { } func (ch *RocketChat) SetConfig(jsonStr json.RawMessage) error { + err := plugin.PopulateDefaults(ch) + if err != nil { + return err + } + return json.Unmarshal(jsonStr, ch) } func (ch *RocketChat) GetInfo() *plugin.Info { - - elements := []*plugin.ConfigOption{ + configAttrs := plugin.ConfigOptions{ { Name: "url", Type: "string", @@ -112,11 +116,6 @@ func (ch *RocketChat) GetInfo() *plugin.Info { }, } - configAttrs, err := json.Marshal(elements) - if err != nil { - panic(err) - } - return &plugin.Info{ Name: "Rocket.Chat", Version: internal.Version.Version, diff --git a/cmd/channels/webhook/main.go b/cmd/channels/webhook/main.go index a5a24635e..88ad80e9f 100644 --- a/cmd/channels/webhook/main.go +++ b/cmd/channels/webhook/main.go @@ -27,7 +27,7 @@ type Webhook struct { } func (ch *Webhook) GetInfo() *plugin.Info { - elements := []*plugin.ConfigOption{ + configAttrs := plugin.ConfigOptions{ { Name: "method", Type: "string", @@ -65,7 +65,7 @@ func (ch *Webhook) GetInfo() *plugin.Info { "en_US": "Go template applied to the current plugin.NotificationRequest to create an request body.", "de_DE": "Go-Template über das zu verarbeitende plugin.NotificationRequest zum Erzeugen der mitgesendeten Anfragedaten.", }, - Default: `{{json .}}`, + Default: "{{json .}}", }, { Name: "response_status_codes", @@ -82,11 +82,6 @@ func (ch *Webhook) GetInfo() *plugin.Info { }, } - configAttrs, err := json.Marshal(elements) - if err != nil { - panic(err) - } - return &plugin.Info{ Name: "Webhook", Version: internal.Version.Version, @@ -96,7 +91,12 @@ func (ch *Webhook) GetInfo() *plugin.Info { } func (ch *Webhook) SetConfig(jsonStr json.RawMessage) error { - err := json.Unmarshal(jsonStr, ch) + err := plugin.PopulateDefaults(ch) + if err != nil { + return err + } + + err = json.Unmarshal(jsonStr, ch) if err != nil { return err } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 0db441559..6d85a508b 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -1,6 +1,7 @@ package plugin import ( + "database/sql/driver" "encoding/json" "errors" "fmt" @@ -69,13 +70,23 @@ type ConfigOption struct { Max types.Int `json:"max,omitempty"` } +// ConfigOptions describes all ConfigOption entries. +// +// This type became necessary to implement the database.sql.driver.Valuer to marshal it into JSON. +type ConfigOptions []ConfigOption + +// Value implements database.sql's driver.Valuer to represent all ConfigOptions as a JSON array. +func (c ConfigOptions) Value() (driver.Value, error) { + return json.Marshal(c) +} + // Info contains plugin information. type Info struct { - Type string `db:"type" json:"-"` - Name string `db:"name" json:"name"` - Version string `db:"version" json:"version"` - Author string `db:"author" json:"author"` - ConfigAttributes json.RawMessage `db:"config_attrs" json:"config_attrs"` // ConfigOption(s) as json-encoded list + Type string `db:"type" json:"-"` + Name string `db:"name" json:"name"` + Version string `db:"version" json:"version"` + Author string `db:"author" json:"author"` + ConfigAttributes ConfigOptions `db:"config_attrs" json:"config_attrs"` } // TableName implements the contracts.TableNamer interface. @@ -131,6 +142,25 @@ type Plugin interface { SendNotification(req *NotificationRequest) error } +// PopulateDefaults sets the struct fields from Info.ConfigAttributes where ConfigOption.Default is set. +// +// It should be called from each channel plugin within its Plugin.SetConfig before doing any further configuration. +func PopulateDefaults(typePtr Plugin) error { + defaults := make(map[string]any) + for _, confAttr := range typePtr.GetInfo().ConfigAttributes { + if confAttr.Default != nil { + defaults[confAttr.Name] = confAttr.Default + } + } + + defaultConf, err := json.Marshal(defaults) + if err != nil { + return err + } + + return json.Unmarshal(defaultConf, typePtr) +} + // RunPlugin reads the incoming stdin requests, processes and writes the responses to stdout func RunPlugin(plugin Plugin) { encoder := json.NewEncoder(os.Stdout)