From a10464ebd4d25c94e1210cc8a65a3d635c759efa Mon Sep 17 00:00:00 2001 From: Marcelo Castagna Date: Mon, 25 May 2020 12:27:12 -0300 Subject: [PATCH] Add Amqp notification client (#63) --- doc/configuration.md | 19 ++++++ doc/notifications.md | 19 +++++- go.mod | 1 + go.sum | 2 + internal/config/config.go | 6 ++ internal/config/config.test.yml | 7 ++ internal/config/config_test.go | 8 +++ internal/model/notif.go | 14 ++++ internal/notif/amqp/client.go | 115 ++++++++++++++++++++++++++++++++ internal/notif/client.go | 4 ++ 10 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 internal/notif/amqp/client.go diff --git a/doc/configuration.md b/doc/configuration.md index ea716180..25a27b27 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -22,6 +22,14 @@ watch: first_check_notif: false notif: + amqp: + enable: false + host: localhost + port: 5672 + username: guest + password: guest + exchange: + queue: queue gotify: enable: false endpoint: http://gotify.foo.com @@ -107,6 +115,17 @@ providers: ### notif +* `amqp` + * `enable`: Enable AMQP notifications (default: `false`). + * `host`: AMQP server host (default: `localhost`). **required** + * `port`: AMQP server port (default: `5672`). **required** + * `username`: AMQP username. **required** + * `username_file`: Use content of secret file as AMQP username if `username` not defined. + * `password`: AMQP password. **required** + * `password_file`: Use content of secret file as AMQP password if `password` not defined. + * `exchange`: Name of the exchange the message will be sent to. (default: `empty`) + * `queue`: Name of the queue the message will be sent to. **required** + * `gotify` * `enable`: Enable gotify notification (default: `false`). * `endpoint`: Gotify base URL (e.g. `http://gotify.foo.com`). **required** diff --git a/doc/notifications.md b/doc/notifications.md index 71567dc0..9ec60857 100644 --- a/doc/notifications.md +++ b/doc/notifications.md @@ -1,5 +1,5 @@ # Notifications - +* [Amqp](#amqp) * [Gotify](#gotify) * [Mail](#mail) * [Rocket.Chat](#rocketchat) @@ -7,6 +7,23 @@ * [Telegram](#telegram) * [Webhook](#webhook) +## Amqp + +You can send notifications to any amqp compatible server, the body will be a JSON format that looks like: + +```json +{ + "diun_version": "0.3.0", + "status": "new", + "provider": "static-0", + "image": "docker.io/crazymax/swarm-cronjob:0.2.1", + "mime_type": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:5913d4b5e8dc15430c2f47f40e43ab2ca7f2b8df5eee5db4d5c42311e08dfb79", + "created": "2019-01-24T10:26:49.152006005Z", + "platform": "linux/amd64", +} +``` + ## Gotify Notifications can be sent using a [Gotify](https://gotify.net/) instance. [Follow the instructions](https://gotify.net/docs/install) to set up a Gotify server. diff --git a/go.mod b/go.mod index a776e835..3d990173 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.18.0 github.com/sirupsen/logrus v1.6.0 + github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 github.com/stretchr/testify v1.5.1 github.com/technoweenie/multipartstreamer v1.0.1 // indirect go.etcd.io/bbolt v1.3.4 diff --git a/go.sum b/go.sum index 957c48d2..a4ec2afc 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,8 @@ github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 h1:2MR0pKUzlP3SGgj5NYJe/zRYDwOu9ku6YHy+Iw7l5DM= +github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/internal/config/config.go b/internal/config/config.go index 7b2cf035..10a9d310 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -48,6 +48,12 @@ func Load(cli model.Cli, version string) (*Config, error) { FirstCheckNotif: false, }, Notif: model.Notif{ + Amqp: model.NotifAmqp{ + Enable: false, + Host: "localhost", + Port: 5672, + Exchange: "", + }, Gotify: model.NotifGotify{ Enable: false, Timeout: 10, diff --git a/internal/config/config.test.yml b/internal/config/config.test.yml index ca6c5b63..b687d57e 100644 --- a/internal/config/config.test.yml +++ b/internal/config/config.test.yml @@ -7,6 +7,13 @@ watch: first_check_notif: false notif: + amqp: + enable: false + host: localhost + port: 5672 + username: guest + password: guest + queue: queue gotify: enable: false endpoint: http://gotify.foo.com diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bab04a2d..168cef26 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -52,6 +52,14 @@ func TestLoad(t *testing.T) { Schedule: "*/30 * * * *", }, Notif: model.Notif{ + Amqp: model.NotifAmqp{ + Enable: false, + Host: "localhost", + Port: 5672, + Username: "guest", + Password: "guest", + Queue: "queue", + }, Gotify: model.NotifGotify{ Enable: false, Endpoint: "http://gotify.foo.com", diff --git a/internal/model/notif.go b/internal/model/notif.go index c56c2713..84b7066c 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -14,6 +14,7 @@ type NotifEntry struct { // Notif holds data necessary for notification configuration type Notif struct { + Amqp NotifAmqp `yaml:"amqp,omitempty"` Gotify NotifGotify `yaml:"gotify,omitempty"` Mail NotifMail `yaml:"mail,omitempty"` RocketChat NotifRocketChat `yaml:"rocketchat,omitempty"` @@ -22,6 +23,19 @@ type Notif struct { Webhook NotifWebhook `yaml:"webhook,omitempty"` } +// NotifAmqp holds amqp notification configuration details +type NotifAmqp struct { + Enable bool `yaml:"enable,omitempty"` + Username string `yaml:"username,omitempty"` + UsernameFile string `yaml:"username_file,omitempty"` + Password string `yaml:"password,omitempty"` + PasswordFile string `yaml:"password_file,omitempty"` + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + Queue string `yaml:"queue,omitempty"` + Exchange string `yaml:"exchange,omitempty"` +} + // NotifGotify holds gotify notification configuration details type NotifGotify struct { Enable bool `yaml:"enable,omitempty"` diff --git a/internal/notif/amqp/client.go b/internal/notif/amqp/client.go new file mode 100644 index 00000000..f7c4980c --- /dev/null +++ b/internal/notif/amqp/client.go @@ -0,0 +1,115 @@ +package amqp + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/crazy-max/diun/internal/model" + "github.com/crazy-max/diun/internal/notif/notifier" + "github.com/crazy-max/diun/pkg/utl" + "github.com/opencontainers/go-digest" + "github.com/streadway/amqp" +) + +// Client represents an active amqp notification object +type Client struct { + *notifier.Notifier + cfg model.NotifAmqp + app model.App +} + +// New creates a new amqp notification instance +func New(config model.NotifAmqp, app model.App) notifier.Notifier { + return notifier.Notifier{ + Handler: &Client{ + cfg: config, + app: app, + }, + } +} + +// Name returns notifier's name +func (c *Client) Name() string { + return "amqp" +} + +// Send creates and sends a amqp 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 + } + + connString := fmt.Sprintf("amqp://%s:%s@%s:%d/", username, password, c.cfg.Host, c.cfg.Port) + + conn, err := amqp.Dial(connString) + if err != nil { + return err + } + + defer conn.Close() + + ch, err := conn.Channel() + if err != nil { + return err + } + + defer ch.Close() + + q, err := ch.QueueDeclare( + c.cfg.Queue, // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + return err + } + + body, err := buildBody(entry, c.app) + if err != nil { + return err + } + + return ch.Publish( + c.cfg.Exchange, // exchange + q.Name, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "application/json", + Body: body, + }) +} + +func buildBody(entry model.NotifEntry, app model.App) ([]byte, error) { + return json.Marshal(struct { + Version string `json:"diun_version"` + Status string `json:"status"` + Provider string `json:"provider"` + Image string `json:"image"` + MIMEType string `json:"mime_type"` + Digest digest.Digest `json:"digest"` + Created *time.Time `json:"created"` + Platform string `json:"platform"` + }{ + Version: app.Version, + Status: string(entry.Status), + Provider: entry.Provider, + Image: entry.Image.String(), + MIMEType: entry.Manifest.MIMEType, + Digest: entry.Manifest.Digest, + Created: entry.Manifest.Created, + Platform: entry.Manifest.Platform, + }) +} diff --git a/internal/notif/client.go b/internal/notif/client.go index efb6a4f4..d1058264 100644 --- a/internal/notif/client.go +++ b/internal/notif/client.go @@ -2,6 +2,7 @@ package notif import ( "github.com/crazy-max/diun/internal/model" + "github.com/crazy-max/diun/internal/notif/amqp" "github.com/crazy-max/diun/internal/notif/gotify" "github.com/crazy-max/diun/internal/notif/mail" "github.com/crazy-max/diun/internal/notif/notifier" @@ -28,6 +29,9 @@ func New(config model.Notif, app model.App, userAgent string) (*Client, error) { } // Add notifiers + if config.Amqp.Enable { + c.notifiers = append(c.notifiers, amqp.New(config.Amqp, app)) + } if config.Gotify.Enable { c.notifiers = append(c.notifiers, gotify.New(config.Gotify, app, userAgent)) }