diff --git a/.res/notif-rocketchat.png b/.res/notif-rocketchat.png new file mode 100644 index 000000000..45889abc9 Binary files /dev/null and b/.res/notif-rocketchat.png differ diff --git a/cmd/main.go b/cmd/main.go index d911d6cf3..5058b22a3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,7 +28,7 @@ func main() { kingpin.Flag("config", "Diun configuration file.").Envar("CONFIG").Required().StringVar(&flags.Cfgfile) kingpin.Flag("timezone", "Timezone assigned to Diun.").Envar("TZ").Default("UTC").StringVar(&flags.Timezone) kingpin.Flag("log-level", "Set log level.").Envar("LOG_LEVEL").Default("info").StringVar(&flags.LogLevel) - kingpin.Flag("log-json", "Enable JSON logging output.").Envar("LOG_JSON").Default("false").BoolVar(&flags.LogJson) + kingpin.Flag("log-json", "Enable JSON logging output.").Envar("LOG_JSON").Default("false").BoolVar(&flags.LogJSON) kingpin.Flag("log-caller", "Enable to add file:line of the caller.").Envar("LOG_CALLER").Default("false").BoolVar(&flags.LogCaller) kingpin.UsageTemplate(kingpin.CompactUsageTemplate).Version(version).Author("CrazyMax") kingpin.CommandLine.Name = "diun" diff --git a/doc/configuration.md b/doc/configuration.md index a64cdca7f..c834c515b 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -22,6 +22,12 @@ watch: first_check_notif: false notif: + gotify: + enable: false + endpoint: http://gotify.foo.com + token: Token123456 + priority: 1 + timeout: 10 mail: enable: false host: localhost @@ -32,6 +38,13 @@ notif: password: from: to: + rocketchat: + enable: false + endpoint: http://rocket.foo.com:3000 + channel: "#general" + user_id: abcdEFGH012345678 + token: Token123456 + timeout: 10 slack: enable: false webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij @@ -49,12 +62,6 @@ notif: Content-Type: application/json Authorization: Token123456 timeout: 10 - gotify: - enable: false - endpoint: http://gotify.foo.com - token: Token123456 - priority: 1 - timeout: 10 regopts: someregistryoptions: @@ -123,6 +130,13 @@ providers: ### notif +* `gotify` + * `enable`: Enable gotify notification (default: `false`). + * `endpoint`: Gotify base URL (e.g. `http://gotify.foo.com`). **required** + * `token`: Application token. **required** + * `priority`: The priority of the message. + * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). + * `mail` * `enable`: Enable email reports (default: `false`). * `host`: SMTP server host (default: `localhost`). **required** @@ -136,6 +150,14 @@ providers: * `from`: Sender email address. **required** * `to`: Recipient email address. **required** +* `rocketchat` + * `enable`: Enable Rocket.Chat notification (default: `false`). + * `endpoint`: Rocket.Chat base URL (e.g. `http://rocket.foo.com:3000`). **required** + * `channel`: Channel name with the prefix in front of it. **required** + * `user_id`: User ID. **required** + * `token`: Authentication token. **required** + * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). + * `slack` * `enable`: Enable slack notification (default: `false`). * `webhook_url`: Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks). **required** @@ -152,13 +174,6 @@ providers: * `headers`: Map of additional headers to be sent. * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). -* `gotify` - * `enable`: Enable gotify notification (default: `false`). - * `endpoint`: Gotify base URL (e.g. `http://gotify.foo.com`). **required** - * `token`: Application token. **required** - * `priority`: The priority of the message. - * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). - ### regopts * `username`: Registry username. diff --git a/doc/notifications.md b/doc/notifications.md index fbf7be08b..ed8b7c427 100644 --- a/doc/notifications.md +++ b/doc/notifications.md @@ -2,6 +2,7 @@ * [Gotify](#gotify) * [Mail](#mail) +* [Rocket.Chat](#rocketchat) * [Slack](#slack) * [Telegram](#telegram) * [Webhook](#webhook) @@ -18,9 +19,15 @@ Here is an email sample if you add `mail` notification: ![](../.res/notif-mail.png) +## Rocket.Chat + +To be able to send notifications to your Rocket.Chat channel, you must first create a Personal Access Token through your account settings: + +![](../.res/notif-rocketchat.png) + ## Slack -You can send notifications to your slack channel using an [incoming webhook URL](https://api.slack.com/messaging/webhooks): +You can send notifications to your Slack channel using an [incoming webhook URL](https://api.slack.com/messaging/webhooks): ![](../.res/notif-slack.png) diff --git a/internal/config/config.go b/internal/config/config.go index 1270ceeae..7efc54786 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,6 +49,10 @@ func Load(flags model.Flags, version string) (*Config, error) { FirstCheckNotif: false, }, Notif: model.Notif{ + Gotify: model.NotifGotify{ + Enable: false, + Timeout: 10, + }, Mail: model.NotifMail{ Enable: false, Host: "localhost", @@ -56,6 +60,10 @@ func Load(flags model.Flags, version string) (*Config, error) { SSL: false, InsecureSkipVerify: false, }, + RocketChat: model.NotifRocketChat{ + Enable: false, + Timeout: 10, + }, Slack: model.NotifSlack{ Enable: false, }, @@ -67,10 +75,6 @@ func Load(flags model.Flags, version string) (*Config, error) { Method: "GET", Timeout: 10, }, - Gotify: model.NotifGotify{ - Enable: false, - Timeout: 10, - }, }, } diff --git a/internal/config/config.test.yml b/internal/config/config.test.yml index 3cf2594e2..03efe58c0 100644 --- a/internal/config/config.test.yml +++ b/internal/config/config.test.yml @@ -7,6 +7,12 @@ watch: first_check_notif: false notif: + gotify: + enable: false + endpoint: http://gotify.foo.com + token: Token123456 + priority: 1 + timeout: 10 mail: enable: false host: localhost @@ -19,6 +25,13 @@ notif: password_file: from: to: + rocketchat: + enable: false + endpoint: http://rocket.foo.com:3000 + channel: "#general" + user_id: abcdEFGH012345678 + token: Token123456 + timeout: 10 slack: enable: false webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij @@ -36,12 +49,6 @@ notif: Content-Type: application/json Authorization: Token123456 timeout: 10 - gotify: - enable: false - endpoint: http://gotify.foo.com - token: Token123456 - priority: 1 - timeout: 10 regopts: someregopts: diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d4f59d814..580f57473 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -52,6 +52,13 @@ func TestLoad(t *testing.T) { Schedule: "*/30 * * * *", }, Notif: model.Notif{ + Gotify: model.NotifGotify{ + Enable: false, + Endpoint: "http://gotify.foo.com", + Token: "Token123456", + Priority: 1, + Timeout: 10, + }, Mail: model.NotifMail{ Enable: false, Host: "localhost", @@ -59,6 +66,14 @@ func TestLoad(t *testing.T) { SSL: false, InsecureSkipVerify: false, }, + RocketChat: model.NotifRocketChat{ + Enable: false, + Endpoint: "http://rocket.foo.com:3000", + Channel: "#general", + UserID: "abcdEFGH012345678", + Token: "Token123456", + Timeout: 10, + }, Slack: model.NotifSlack{ Enable: false, WebhookURL: "https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij", @@ -78,13 +93,6 @@ func TestLoad(t *testing.T) { }, Timeout: 10, }, - Gotify: model.NotifGotify{ - Enable: false, - Endpoint: "http://gotify.foo.com", - Token: "Token123456", - Priority: 1, - Timeout: 10, - }, }, RegOpts: map[string]model.RegOpts{ "someregopts": { diff --git a/internal/logging/logger.go b/internal/logging/logger.go index ec82be037..05e21a2cb 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -21,7 +21,7 @@ func Configure(fl *model.Flags, location *time.Location) { return time.Now().In(location) } - if !fl.LogJson { + if !fl.LogJSON { w = zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: time.RFC1123, diff --git a/internal/model/flags.go b/internal/model/flags.go index d4b636e8c..3f0585202 100644 --- a/internal/model/flags.go +++ b/internal/model/flags.go @@ -5,6 +5,6 @@ type Flags struct { Cfgfile string Timezone string LogLevel string - LogJson bool + LogJSON bool LogCaller bool } diff --git a/internal/model/notif.go b/internal/model/notif.go index 2539fdda0..c56c2713a 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -14,11 +14,21 @@ type NotifEntry struct { // Notif holds data necessary for notification configuration type Notif struct { - Mail NotifMail `yaml:"mail,omitempty"` - Slack NotifSlack `yaml:"slack,omitempty"` - Telegram NotifTelegram `yaml:"telegram,omitempty"` - Webhook NotifWebhook `yaml:"webhook,omitempty"` - Gotify NotifGotify `yaml:"gotify,omitempty"` + Gotify NotifGotify `yaml:"gotify,omitempty"` + Mail NotifMail `yaml:"mail,omitempty"` + RocketChat NotifRocketChat `yaml:"rocketchat,omitempty"` + Slack NotifSlack `yaml:"slack,omitempty"` + Telegram NotifTelegram `yaml:"telegram,omitempty"` + Webhook NotifWebhook `yaml:"webhook,omitempty"` +} + +// NotifGotify holds gotify notification configuration details +type NotifGotify struct { + Enable bool `yaml:"enable,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` + Token string `yaml:"token,omitempty"` + Priority int `yaml:"priority,omitempty"` + Timeout int `yaml:"timeout,omitempty"` } // NotifMail holds mail notification configuration details @@ -36,6 +46,16 @@ type NotifMail struct { To string `yaml:"to,omitempty"` } +// NotifRocketChat holds Rocket.Chat notification configuration details +type NotifRocketChat struct { + Enable bool `yaml:"enable,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` + Channel string `yaml:"channel,omitempty"` + UserID string `yaml:"user_id,omitempty"` + Token string `yaml:"token,omitempty"` + Timeout int `yaml:"timeout,omitempty"` +} + // NotifSlack holds slack notification configuration details type NotifSlack struct { Enable bool `yaml:"enable,omitempty"` @@ -57,12 +77,3 @@ type NotifWebhook struct { Headers map[string]string `yaml:"headers,omitempty"` Timeout int `yaml:"timeout,omitempty"` } - -// NotifGotify holds gotify notification configuration details -type NotifGotify struct { - Enable bool `yaml:"enable,omitempty"` - Endpoint string `yaml:"endpoint,omitempty"` - Token string `yaml:"token,omitempty"` - Priority int `yaml:"priority,omitempty"` - Timeout int `yaml:"timeout,omitempty"` -} diff --git a/internal/notif/client.go b/internal/notif/client.go index c68b7b1da..efb6a4f45 100644 --- a/internal/notif/client.go +++ b/internal/notif/client.go @@ -5,6 +5,7 @@ import ( "github.com/crazy-max/diun/internal/notif/gotify" "github.com/crazy-max/diun/internal/notif/mail" "github.com/crazy-max/diun/internal/notif/notifier" + "github.com/crazy-max/diun/internal/notif/rocketchat" "github.com/crazy-max/diun/internal/notif/slack" "github.com/crazy-max/diun/internal/notif/telegram" "github.com/crazy-max/diun/internal/notif/webhook" @@ -27,9 +28,15 @@ func New(config model.Notif, app model.App, userAgent string) (*Client, error) { } // Add notifiers + if config.Gotify.Enable { + c.notifiers = append(c.notifiers, gotify.New(config.Gotify, app, userAgent)) + } if config.Mail.Enable { c.notifiers = append(c.notifiers, mail.New(config.Mail, app)) } + if config.RocketChat.Enable { + c.notifiers = append(c.notifiers, rocketchat.New(config.RocketChat, app, userAgent)) + } if config.Slack.Enable { c.notifiers = append(c.notifiers, slack.New(config.Slack, app)) } @@ -39,9 +46,6 @@ func New(config model.Notif, app model.App, userAgent string) (*Client, error) { if config.Webhook.Enable { c.notifiers = append(c.notifiers, webhook.New(config.Webhook, app, userAgent)) } - if config.Gotify.Enable { - c.notifiers = append(c.notifiers, gotify.New(config.Gotify, app, userAgent)) - } log.Debug().Msgf("%d notifier(s) created", len(c.notifiers)) return c, nil diff --git a/internal/notif/rocketchat/client.go b/internal/notif/rocketchat/client.go new file mode 100644 index 000000000..c6c833c24 --- /dev/null +++ b/internal/notif/rocketchat/client.go @@ -0,0 +1,136 @@ +package rocketchat + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strconv" + "text/template" + "time" + + "github.com/crazy-max/diun/internal/model" + "github.com/crazy-max/diun/internal/notif/notifier" +) + +// Client represents an active rocketchat notification object +type Client struct { + *notifier.Notifier + cfg model.NotifRocketChat + app model.App + userAgent string +} + +// New creates a new rocketchat notification instance +func New(config model.NotifRocketChat, app model.App, userAgent string) notifier.Notifier { + return notifier.Notifier{ + Handler: &Client{ + cfg: config, + app: app, + userAgent: userAgent, + }, + } +} + +// Name returns notifier's name +func (c *Client) Name() string { + return "rocketchat" +} + +// Send creates and sends a rocketchat notification with an entry +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ +func (c *Client) Send(entry model.NotifEntry) error { + hc := http.Client{ + Timeout: time.Duration(c.cfg.Timeout) * time.Second, + } + + title := fmt.Sprintf("Image update for %s", entry.Image.String()) + if entry.Status == model.ImageStatusNew { + title = fmt.Sprintf("New image %s has been added", entry.Image.String()) + } + + var textBuf bytes.Buffer + textTpl := template.Must(template.New("rocketchat").Parse(`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 }}.`)) + if err := textTpl.Execute(&textBuf, entry); err != nil { + return err + } + + data := Message{ + Alias: c.app.Name, + Avatar: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png", + Channel: c.cfg.Channel, + Text: title, + Attachments: []Attachment{ + { + Text: textBuf.String(), + Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)), + Fields: []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, + }, + }, + }, + }, + } + + dataBuf := new(bytes.Buffer) + if err := json.NewEncoder(dataBuf).Encode(data); err != nil { + return err + } + + u, err := url.Parse(c.cfg.Endpoint) + if err != nil { + return err + } + u.Path = path.Join(u.Path, "api/v1/chat.postMessage") + + req, err := http.NewRequest("POST", u.String(), dataBuf) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", c.userAgent) + req.Header.Add("X-User-Id", c.cfg.UserID) + req.Header.Add("X-Auth-Token", c.cfg.Token) + + resp, err := hc.Do(req) + if err != nil { + return err + } + + var respBody struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + ErrorType string `json:"errorType,omitempty"` + } + err = json.NewDecoder(resp.Body).Decode(&respBody) + if err == nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, respBody.ErrorType) + } + + return nil +} diff --git a/internal/notif/rocketchat/model.go b/internal/notif/rocketchat/model.go new file mode 100644 index 000000000..89724f467 --- /dev/null +++ b/internal/notif/rocketchat/model.go @@ -0,0 +1,41 @@ +package rocketchat + +import "encoding/json" + +// Message contains all the information for a message +type Message struct { + Alias string `json:"alias,omitempty"` + Avatar string `json:"avatar,omitempty"` + Channel string `json:"channel,omitempty"` + Emoji string `json:"emoji,omitempty"` + RoomID string `json:"roomId,omitempty"` + Text string `json:"text,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +// Attachment contains all the information for an attachment +type Attachment struct { + AudioURL string `json:"audio_url,omitempty"` + AuthorIcon string `json:"author_icon,omitempty"` + AuthorLink string `json:"author_link,omitempty"` + AuthorName string `json:"author_name,omitempty"` + Collapsed bool `json:"collapsed,omitempty"` + Color bool `json:"color,omitempty"` + Fields []AttachmentField `json:"fields,omitempty"` + ImageURL string `json:"image_url,omitempty"` + MessageLink string `json:"message_link,omitempty"` + Text string `json:"text"` + ThumbURL string `json:"thumb_url,omitempty"` + Title string `json:"title,omitempty"` + TitleLink string `json:"title_link,omitempty"` + Ts json.Number `json:"ts,omitempty"` + VideoURL string `json:"video_url,omitempty"` +} + +// AttachmentField contains information for an attachment field +// An Attachment can contain multiple of these +type AttachmentField struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} diff --git a/internal/notif/slack/slack.go b/internal/notif/slack/slack.go index 8c9eddf82..9d3a5986b 100644 --- a/internal/notif/slack/slack.go +++ b/internal/notif/slack/slack.go @@ -49,37 +49,39 @@ func (c *Client) Send(entry model.NotifEntry) error { } 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, + Attachments: []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)), }, - Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)), - }}, + }, }) } diff --git a/pkg/registry/image.go b/pkg/registry/image.go index d16dfd004..c0e1c4b0a 100644 --- a/pkg/registry/image.go +++ b/pkg/registry/image.go @@ -1,4 +1,3 @@ -// Source: https://github.com/genuinetools/reg/blob/f3a9b00ec86f334702381edf842f03b3a9243a0a/registry/image.go package registry import ( @@ -8,6 +7,8 @@ import ( digest "github.com/opencontainers/go-digest" ) +// Source: https://github.com/genuinetools/reg/blob/f3a9b00ec86f334702381edf842f03b3a9243a0a/registry/image.go + // Image holds information about an image. type Image struct { Domain string diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go index 5f9d1a1cd..1da417a29 100644 --- a/pkg/registry/manifest.go +++ b/pkg/registry/manifest.go @@ -7,6 +7,7 @@ import ( "github.com/opencontainers/go-digest" ) +// Manifest is the Docker image manifest information type Manifest struct { Name string Tag string diff --git a/pkg/registry/tags.go b/pkg/registry/tags.go index 23df2043d..e0ae1880f 100644 --- a/pkg/registry/tags.go +++ b/pkg/registry/tags.go @@ -5,6 +5,7 @@ import ( "github.com/crazy-max/diun/pkg/utl" ) +// Tags holds information about image tags. type Tags struct { List []string NotIncluded int @@ -12,6 +13,7 @@ type Tags struct { Total int } +// TagsOptions holds docker tags image options type TagsOptions struct { Image Image Max int