diff --git a/.res/notif-slack.png b/.res/notif-slack.png new file mode 100644 index 000000000..c2f46295c Binary files /dev/null and b/.res/notif-slack.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 13556217f..87d15e323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.1.0 (2019/12/17) + +* Add Slack notifier (#8) + ## 2.0.0 (2019/12/14) * Include provider in notifications diff --git a/doc/configuration.md b/doc/configuration.md index e35557dca..9203f1ab9 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -21,6 +21,9 @@ notif: password: from: to: + slack: + enable: false + webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij webhook: enable: false endpoint: http://webhook.foo.com/sd54qad89azd5a @@ -107,6 +110,10 @@ providers: * `from`: Sender email address. **required** * `to`: Recipient email address. **required** +* `slack` + * `enable`: Enable slack notification (default: `false`). + * `webhook_url`: Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks). **required** + * `webhook` * `enable`: Enable webhook notification (default: `false`). * `endpoint`: URL of the HTTP request. **required** diff --git a/doc/notifications.md b/doc/notifications.md index 2216c1aef..16b58ad58 100644 --- a/doc/notifications.md +++ b/doc/notifications.md @@ -1,6 +1,7 @@ # Notifications * [Mail](#mail) +* [Slack](#slack) * [Webhook](#webhook) ## Mail @@ -9,6 +10,12 @@ Here is an email sample if you add `mail` notification: ![](../.res/notif-mail.png) +## Slack + +You can send notifications to your slack channel using an [incoming webhook URL](https://api.slack.com/messaging/webhooks): + +![](../.res/notif-slack.png) + ## Webhook If you choose `webhook` notification, a HTTP request is sent with a JSON format response that looks like: diff --git a/go.mod b/go.mod index f3f9a2ac9..100c4016e 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/imdario/mergo v0.3.8 github.com/matcornic/hermes/v2 v2.0.2 github.com/morikuni/aec v1.0.0 // indirect + github.com/nlopes/slack v0.6.0 github.com/opencontainers/go-digest v1.0.0-rc1 github.com/opencontainers/image-spec v1.0.1 // indirect github.com/panjf2000/ants/v2 v2.2.2 diff --git a/go.sum b/go.sum index fec4897d9..d4a30733e 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,8 @@ github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84 h1:RvcDqcKLua4b/jtXez7ZVe9s6Iq5N6ujVevqY4FBQmM= github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -136,6 +138,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= +github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= diff --git a/internal/config/config.go b/internal/config/config.go index 9ae4c203a..db7863860 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -48,14 +48,17 @@ func Load(flags model.Flags, version string) (*Config, error) { Schedule: "0 * * * *", }, Notif: model.Notif{ - Mail: model.Mail{ + Mail: model.NotifMail{ Enable: false, Host: "localhost", Port: 25, SSL: false, InsecureSkipVerify: false, }, - Webhook: model.Webhook{ + Slack: model.NotifSlack{ + Enable: false, + }, + Webhook: model.NotifWebhook{ Enable: false, Method: "GET", Timeout: 10, diff --git a/internal/config/config.test.yml b/internal/config/config.test.yml index 227897c5a..e05250ef9 100644 --- a/internal/config/config.test.yml +++ b/internal/config/config.test.yml @@ -18,6 +18,9 @@ notif: password_file: from: to: + slack: + enable: false + webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij webhook: enable: false endpoint: http://webhook.foo.com/sd54qad89azd5a diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f1416e29e..7c31ae879 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -52,14 +52,18 @@ func TestLoad(t *testing.T) { Schedule: "*/30 * * * *", }, Notif: model.Notif{ - Mail: model.Mail{ + Mail: model.NotifMail{ Enable: false, Host: "localhost", Port: 25, SSL: false, InsecureSkipVerify: false, }, - Webhook: model.Webhook{ + Slack: model.NotifSlack{ + Enable: false, + WebhookURL: "https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij", + }, + Webhook: model.NotifWebhook{ Enable: false, Endpoint: "http://webhook.foo.com/sd54qad89azd5a", Method: "GET", diff --git a/internal/model/mail.go b/internal/model/mail.go deleted file mode 100644 index 0b15d4d3d..000000000 --- a/internal/model/mail.go +++ /dev/null @@ -1,16 +0,0 @@ -package model - -// Mail holds mail notification configuration details -type Mail struct { - Enable bool `yaml:"enable,omitempty"` - Host string `yaml:"host,omitempty"` - Port int `yaml:"port,omitempty"` - SSL bool `yaml:"ssl,omitempty"` - InsecureSkipVerify bool `yaml:"insecure_skip_verify,omitempty"` - Username string `yaml:"username,omitempty"` - UsernameFile string `yaml:"username_file,omitempty"` - Password string `yaml:"password,omitempty"` - PasswordFile string `yaml:"password_file,omitempty"` - From string `yaml:"from,omitempty"` - To string `yaml:"to,omitempty"` -} diff --git a/internal/model/notif.go b/internal/model/notif.go index a3f80ba41..0d1367556 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -5,12 +5,6 @@ import ( "github.com/crazy-max/diun/pkg/docker/registry" ) -// Notif holds data necessary for notification configuration -type Notif struct { - Mail Mail `yaml:"mail,omitempty"` - Webhook Webhook `yaml:"webhook,omitempty"` -} - // NotifEntry represents a notification entry type NotifEntry struct { Status ImageStatus `json:"status,omitempty"` @@ -18,3 +12,40 @@ type NotifEntry struct { Image registry.Image `json:"image,omitempty"` Manifest docker.Manifest `json:"manifest,omitempty"` } + +// Notif holds data necessary for notification configuration +type Notif struct { + Mail NotifMail `yaml:"mail,omitempty"` + Slack NotifSlack `yaml:"slack,omitempty"` + Webhook NotifWebhook `yaml:"webhook,omitempty"` +} + +// NotifMail holds mail notification configuration details +type NotifMail struct { + Enable bool `yaml:"enable,omitempty"` + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + SSL bool `yaml:"ssl,omitempty"` + InsecureSkipVerify bool `yaml:"insecure_skip_verify,omitempty"` + Username string `yaml:"username,omitempty"` + UsernameFile string `yaml:"username_file,omitempty"` + Password string `yaml:"password,omitempty"` + PasswordFile string `yaml:"password_file,omitempty"` + From string `yaml:"from,omitempty"` + To string `yaml:"to,omitempty"` +} + +// NotifSlack holds slack notification configuration details +type NotifSlack struct { + Enable bool `yaml:"enable,omitempty"` + WebhookURL string `yaml:"webhook_url,omitempty"` +} + +// NotifWebhook holds webhook notification configuration details +type NotifWebhook struct { + Enable bool `yaml:"enable,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` + Method string `yaml:"method,omitempty"` + Headers map[string]string `yaml:"headers,omitempty"` + Timeout int `yaml:"timeout,omitempty"` +} diff --git a/internal/model/webhook.go b/internal/model/webhook.go deleted file mode 100644 index 301fbb0e1..000000000 --- a/internal/model/webhook.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -// Webhook holds webhook notification configuration details -type Webhook struct { - Enable bool `yaml:"enable,omitempty"` - Endpoint string `yaml:"endpoint,omitempty"` - Method string `yaml:"method,omitempty"` - Headers map[string]string `yaml:"headers,omitempty"` - Timeout int `yaml:"timeout,omitempty"` -} diff --git a/internal/notif/client.go b/internal/notif/client.go index e381934d5..a53ce0a1a 100644 --- a/internal/notif/client.go +++ b/internal/notif/client.go @@ -4,6 +4,7 @@ import ( "github.com/crazy-max/diun/internal/model" "github.com/crazy-max/diun/internal/notif/mail" "github.com/crazy-max/diun/internal/notif/notifier" + "github.com/crazy-max/diun/internal/notif/slack" "github.com/crazy-max/diun/internal/notif/webhook" "github.com/rs/zerolog/log" ) @@ -27,6 +28,9 @@ func New(config model.Notif, app model.App) (*Client, error) { if config.Mail.Enable { c.notifiers = append(c.notifiers, mail.New(config.Mail, app)) } + if config.Slack.Enable { + c.notifiers = append(c.notifiers, slack.New(config.Slack, app)) + } if config.Webhook.Enable { c.notifiers = append(c.notifiers, webhook.New(config.Webhook, app)) } diff --git a/internal/notif/mail/client.go b/internal/notif/mail/client.go index 6209ed2dc..b5b6b2ce5 100644 --- a/internal/notif/mail/client.go +++ b/internal/notif/mail/client.go @@ -18,12 +18,12 @@ import ( // Client represents an active mail notification object type Client struct { *notifier.Notifier - cfg model.Mail + cfg model.NotifMail app model.App } // New creates a new mail notification instance -func New(config model.Mail, app model.App) notifier.Notifier { +func New(config model.NotifMail, app model.App) notifier.Notifier { return notifier.Notifier{ Handler: &Client{ cfg: config, @@ -65,7 +65,7 @@ func (c *Client) Send(entry model.NotifEntry) error { Docker 🐳 tag **{{ .Image.Domain }}/{{ .Image.Path }}:{{ .Image.Tag }}** which you subscribed to through **{{ .Provider }}** provider has been {{ if (eq .Status "new") }}newly added{{ else }}updated{{ end }}. -This image has been {{ if (eq .Status "new") }}created{{ else }}updated{{ end }} at {{ .Manifest.Created }} with digest {{ .Manifest.Digest }} for {{ .Manifest.Os }}/{{ .Manifest.Architecture }} platform. +This image has been {{ if (eq .Status "new") }}created{{ else }}updated{{ end }} at {{ .Manifest.Created.Format "Jan 02, 2006 15:04:05 UTC" }} with digest {{ .Manifest.Digest }} for {{ .Manifest.Os }}/{{ .Manifest.Architecture }} platform. Need help, or have questions? Go to https://github.com/crazy-max/diun and leave an issue. diff --git a/internal/notif/slack/slack.go b/internal/notif/slack/slack.go new file mode 100644 index 000000000..5327ac622 --- /dev/null +++ b/internal/notif/slack/slack.go @@ -0,0 +1,85 @@ +package slack + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "text/template" + "time" + + "github.com/crazy-max/diun/internal/model" + "github.com/crazy-max/diun/internal/notif/notifier" + "github.com/nlopes/slack" +) + +// Client represents an active slack notification object +type Client struct { + *notifier.Notifier + cfg model.NotifSlack + app model.App +} + +// New creates a new slack notification instance +func New(config model.NotifSlack, 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 "slack" +} + +// Send creates and sends a webhook notification with an entry +func (c *Client) Send(entry model.NotifEntry) error { + var textBuf bytes.Buffer + textTpl := template.Must(template.New("text").Parse(" Docker tag `{{ .Image.Domain }}/{{ .Image.Path }}:{{ .Image.Tag }}` {{ if (eq .Status \"new\") }}newly added{{ else }}updated{{ end }}.")) + if err := textTpl.Execute(&textBuf, entry); err != nil { + return err + } + + color := "#4caf50" + if entry.Status == model.ImageStatusUpdate { + color = "#0054ca" + } + + return slack.PostWebhook(c.cfg.WebhookURL, &slack.WebhookMessage{ + Attachments: []slack.Attachment{slack.Attachment{ + Color: color, + AuthorName: "Diun", + AuthorSubname: "github.com/crazy-max/diun", + AuthorLink: "https://github.com/crazy-max/diun", + AuthorIcon: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png", + Text: textBuf.String(), + Footer: fmt.Sprintf("%s © %d %s %s", c.app.Author, time.Now().Year(), c.app.Name, c.app.Version), + Fields: []slack.AttachmentField{ + { + Title: "Provider", + Value: entry.Provider, + Short: false, + }, + { + Title: "Created", + Value: entry.Manifest.Created.Format("Jan 02, 2006 15:04:05 UTC"), + Short: false, + }, + { + Title: "Digest", + Value: entry.Manifest.Digest.String(), + Short: false, + }, + { + Title: "Platform", + Value: fmt.Sprintf("%s/%s", entry.Manifest.Os, entry.Manifest.Architecture), + Short: false, + }, + }, + Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)), + }}, + }) +} diff --git a/internal/notif/webhook/client.go b/internal/notif/webhook/client.go index a3054986e..8267f64f5 100644 --- a/internal/notif/webhook/client.go +++ b/internal/notif/webhook/client.go @@ -15,12 +15,12 @@ import ( // Client represents an active webhook notification object type Client struct { *notifier.Notifier - cfg model.Webhook + cfg model.NotifWebhook app model.App } // New creates a new webhook notification instance -func New(config model.Webhook, app model.App) notifier.Notifier { +func New(config model.NotifWebhook, app model.App) notifier.Notifier { return notifier.Notifier{ Handler: &Client{ cfg: config,