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,