diff --git a/cmd/channels/email/main.go b/cmd/channels/email/main.go index 5119d535..87580f5b 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 00000000..7bbb4a05 --- /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-null-equals-defaults", + jsonMsg: `{"sender_mail": null}`, + 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: "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 143c5b9e..b441234d 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 a5a24635..88ad80e9 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 0db44155..6d85a508 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)