diff --git a/docs/config/index.md b/docs/config/index.md index 89977c0..27108aa 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -205,6 +205,7 @@ Can be transposed to: * [gotify](../notif/gotify.md) * [mail](../notif/mail.md) * [matrix](../notif/matrix.md) + * [mqtt](../notif/mqtt.md) * [rocketchat](../notif/rocketchat.md) * [script](../notif/script.md) * [slack](../notif/slack.md) diff --git a/docs/config/notif.md b/docs/config/notif.md index 4cb8543..3326294 100644 --- a/docs/config/notif.md +++ b/docs/config/notif.md @@ -5,6 +5,7 @@ * [`gotify`](../notif/gotify.md) * [`mail`](../notif/mail.md) * [`matrix`](../notif/matrix.md) +* [`mqtt`](../notif/mqtt.md) * [`rocketchat`](../notif/rocketchat.md) * [`script`](../notif/script.md) * [`slack`](../notif/slack.md) diff --git a/docs/notif/mqtt.md b/docs/notif/mqtt.md new file mode 100644 index 0000000..414f4dc --- /dev/null +++ b/docs/notif/mqtt.md @@ -0,0 +1,51 @@ +# Mqtt notifications + +You can send notifications to any mqtt compatible server with the following settings. + +## Configuration + +!!! example "File" + ```yaml + notif: + mqtt: + host: localhost + port: 1883 + username: guest + password: guest + topic: docker/diun + client: diun + qos: 0 + ``` + +| Name | Default | Description | +|--------------------|---------------|---------------| +| `host`[^1] | `localhost` | MQTT server host | +| `port`[^1] | `1883` | MQTT server port | +| `client`[^1] | `diun-client` | Name of the client which connects to the server | +| `topic`[^1] | `docker/diun` | Topic the message will be sent to | +| `username` | | MQTT username | +| `usernameFile` | | Use content of secret file as MQTT username if `username` not defined | +| `password` | | MQTT password | +| `passwordFile` | | Use content of secret file as MQTT password if `password` not defined | +| `qos` | `0` | Topic the message will be sent to | + +## Sample + +The JSON response will look like this: + +```json +{ + "diun_version": "0.3.0", + "hostname": "myserver", + "status": "new", + "provider": "file", + "image": "docker.io/crazymax/swarm-cronjob:0.2.1", + "hub_link": "https://hub.docker.com/r/crazymax/swarm-cronjob", + "mime_type": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:5913d4b5e8dc15430c2f47f40e43ab2ca7f2b8df5eee5db4d5c42311e08dfb79", + "created": "2019-01-24T10:26:49.152006005Z", + "platform": "linux/amd64" +} +``` + +[^1]: Value required diff --git a/go.mod b/go.mod index 5e6fdd6..12dab8f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/crazy-max/gonfig v0.3.0 github.com/docker/docker v1.4.2-0.20200204220554-5f6d6f3f2203 github.com/docker/go-connections v0.4.0 + github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df github.com/go-playground/validator/v10 v10.4.1 github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible diff --git a/go.sum b/go.sum index e6d6790..bdd7da3 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,8 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/internal/model/notif.go b/internal/model/notif.go index add88dd..154ee6d 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -29,6 +29,7 @@ type Notif struct { Gotify *NotifGotify `yaml:"gotify,omitempty" json:"gotify,omitempty"` Mail *NotifMail `yaml:"mail,omitempty" json:"mail,omitempty"` Matrix *NotifMatrix `yaml:"matrix,omitempty" json:"matrix,omitempty"` + Mqtt *NotifMqtt `yaml:"mqtt,omitempty" json:"mqtt,omitempty"` RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty" json:"rocketchat,omitempty"` Script *NotifScript `yaml:"script,omitempty" json:"script,omitempty"` Slack *NotifSlack `yaml:"slack,omitempty" json:"slack,omitempty"` diff --git a/internal/model/notif_mqtt.go b/internal/model/notif_mqtt.go new file mode 100644 index 0000000..393058d --- /dev/null +++ b/internal/model/notif_mqtt.go @@ -0,0 +1,29 @@ +package model + +type NotifMqtt struct { + Username string `yaml:"username,omitempty" json:"username,omitempty" validate:"omitempty"` + UsernameFile string `yaml:"usernameFile,omitempty" json:"usernameFile,omitempty" validate:"omitempty,file"` + Password string `yaml:"password,omitempty" json:"password,omitempty" validate:"omitempty"` + PasswordFile string `yaml:"passwordFile,omitempty" json:"passwordFile,omitempty" validate:"omitempty,file"` + Host string `yaml:"host,omitempty" json:"host,omitempty" validate:"required"` + Port int `yaml:"port,omitempty" json:"port,omitempty" validate:"required,min=1"` + Topic string `yaml:"topic,omitempty" json:"topic,omitempty" validate:"required"` + Client string `yaml:"client,omitempty" json:"client,omitempty" validate:"required"` + QoS int `yaml:"qos,omitempty" json:"qos,omitempty" validate:"omitempty"` +} + +// GetDefaults gets the default values +func (s *NotifMqtt) GetDefaults() *NotifMqtt { + n := &NotifMqtt{} + n.SetDefaults() + return n +} + +// SetDefaults sets the default values +func (s *NotifMqtt) SetDefaults() { + s.Host = "localhost" + s.Port = 1883 + s.Topic = "docker/diun" + s.Client = "diun-client" + s.QoS = 0 +} diff --git a/internal/notif/client.go b/internal/notif/client.go index a788b6c..6e40b12 100644 --- a/internal/notif/client.go +++ b/internal/notif/client.go @@ -1,6 +1,7 @@ package notif import ( + "github.com/crazy-max/diun/v4/internal/notif/mqtt" "strings" "github.com/crazy-max/diun/v4/internal/model" @@ -55,6 +56,9 @@ func New(config *model.Notif, meta model.Meta) (*Client, error) { if config.Matrix != nil { c.notifiers = append(c.notifiers, matrix.New(config.Matrix, meta)) } + if config.Mqtt != nil { + c.notifiers = append(c.notifiers, mqtt.New(config.Mqtt, meta)) + } if config.RocketChat != nil { c.notifiers = append(c.notifiers, rocketchat.New(config.RocketChat, meta)) } diff --git a/internal/notif/mqtt/client.go b/internal/notif/mqtt/client.go new file mode 100644 index 0000000..1f83f96 --- /dev/null +++ b/internal/notif/mqtt/client.go @@ -0,0 +1,94 @@ +package mqtt + +import ( + "encoding/json" + "fmt" + "github.com/crazy-max/diun/v4/pkg/utl" + "time" + + "github.com/crazy-max/diun/v4/internal/model" + "github.com/crazy-max/diun/v4/internal/notif/notifier" + MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// Client represents an active mqtt notification object +type Client struct { + *notifier.Notifier + cfg *model.NotifMqtt + meta model.Meta + logger zerolog.Logger +} + +// New creates a new mqtt notification instance +func New(config *model.NotifMqtt, meta model.Meta) notifier.Notifier { + return notifier.Notifier{ + Handler: &Client{ + cfg: config, + meta: meta, + logger: log.With().Str("notif", "mqtt").Logger(), + }, + } +} + +// Name returns notifier's name +func (c *Client) Name() string { + return "mqtt" +} + +// Send creates and sends a mqtt notification with an entry +func (c *Client) Send(entry model.NotifEntry) error { + username, err := utl.GetSecret(c.cfg.Username, c.cfg.UsernameFile) + if err != nil { + return err + } + + password, err := utl.GetSecret(c.cfg.Password, c.cfg.PasswordFile) + if err != nil { + return err + } + + broker := fmt.Sprintf("tcp://%s:%d", c.cfg.Host, c.cfg.Port) + opts := MQTT.NewClientOptions().AddBroker(broker).SetClientID(c.cfg.Client) + opts.Username = username + opts.Password = password + + var client = MQTT.NewClient(opts) + if token := client.Connect(); token.Wait() && token.Error() != nil { + return token.Error() + } + + log.Debug().Msgf("Connected to broker: %s", broker) + + message, err := json.Marshal(struct { + Version string `json:"diun_version"` + Hostname string `json:"hostname"` + Status string `json:"status"` + Provider string `json:"provider"` + Image string `json:"image"` + HubLink string `json:"hub_link"` + MIMEType string `json:"mime_type"` + Created *time.Time `json:"created"` + Platform string `json:"platform"` + }{ + Version: c.meta.Version, + Hostname: c.meta.Hostname, + Status: string(entry.Status), + Provider: entry.Provider, + Image: entry.Image.String(), + HubLink: entry.Image.HubLink, + MIMEType: entry.Manifest.MIMEType, + Created: entry.Manifest.Created, + Platform: entry.Manifest.Platform, + }) + if err != nil { + return err + } + + log.Debug().Msgf("Publishing to topic: %s", c.cfg.Topic) + token := client.Publish(c.cfg.Topic, byte(c.cfg.QoS), false, message) + token.Wait() + + return token.Error() +}