diff --git a/docs/notifications.md b/docs/notifications.md index 02f144a..853ee63 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -1,4 +1,4 @@ - + # Notifications Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging system, [logrus](http://github.com/sirupsen/logrus). @@ -7,6 +7,7 @@ The types of notifications to send are passed via the comma-separated option `-- - `email` to send notifications via e-mail - `slack` to send notifications through a Slack webhook - `msteams` to send notifications via MSTeams webhook +- `gotify` to send notifications via Gotify ## Settings @@ -90,3 +91,18 @@ docker run -d \ -e WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true \ containrrr/watchtower ``` + +### Gotify + +To push a notification to your Gotify instance, register a Gotify app and specify the Gotify URL and app token: + + +```bash +docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATIONS=gotify \ + -e WATCHTOWER_NOTIFICATION_GOTIFY_URL="https://my.gotify.tld/" \ + -e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN="SuperSecretToken" \ + containrrr/watchtower +``` diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 645dcac..ae786cd 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -98,7 +98,7 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { "notifications", "n", viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), - " notification types to send (valid: email, slack, msteams") + " notification types to send (valid: email, slack, msteams, gotify)") flags.StringP( "notifications-level", @@ -192,6 +192,17 @@ Should only be used for testing. "", viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), "The MSTeams notifier will try to extract log entry fields as MSTeams message facts") + + flags.StringP( + "notification-gotify-url", + "", + viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), + "The Gotify URL to send notifications to") + flags.StringP( + "notification-gotify-token", + "", + viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), + "The Gotify Application required to query the Gotify API") } // SetDefaults provides default values for environment variables diff --git a/notifications/gotify.go b/notifications/gotify.go new file mode 100644 index 0000000..733bddd --- /dev/null +++ b/notifications/gotify.go @@ -0,0 +1,100 @@ +package notifications + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const ( + gotifyType = "gotify" +) + +type gotifyTypeNotifier struct { + gotifyURL string + gotifyAppToken string + logLevels []log.Level +} + +func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) typeNotifier { + flags := c.PersistentFlags() + + gotifyURL, _ := flags.GetString("notification-gotify-url") + if len(gotifyURL) < 1 { + log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.") + } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) { + log.Fatal("Gotify URL must start with \"http://\" or \"https://\"") + } else if strings.HasPrefix(gotifyURL, "http://") { + log.Warn("Using an HTTP url fpr Gotify is insecure") + } + + gotifyToken, _ := flags.GetString("notification-gotify-token") + if len(gotifyToken) < 1 { + log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.") + } + + n := &gotifyTypeNotifier{ + gotifyURL: gotifyURL, + gotifyAppToken: gotifyToken, + logLevels: acceptedLogLevels, + } + + log.AddHook(n) + + return n +} + +func (n *gotifyTypeNotifier) StartNotification() {} + +func (n *gotifyTypeNotifier) SendNotification() {} + +func (n *gotifyTypeNotifier) Levels() []log.Level { + return n.logLevels +} + +func (n *gotifyTypeNotifier) getURL() string { + url := n.gotifyURL + if !strings.HasSuffix(url, "/") { + url += "/" + } + return url + "message?token=" + n.gotifyAppToken +} + +func (n *gotifyTypeNotifier) Fire(entry *log.Entry) error { + + go func() { + jsonBody, err := json.Marshal(gotifyMessage{ + Message: "(" + entry.Level.String() + "): " + entry.Message, + Title: "Watchtower", + Priority: 0, + }) + if err != nil { + fmt.Println("Failed to create JSON body for Gotify notification: ", err) + return + } + + jsonBodyBuffer := bytes.NewBuffer([]byte(jsonBody)) + resp, err := http.Post(n.getURL(), "application/json", jsonBodyBuffer) + if err != nil { + fmt.Println("Failed to send Gotify notification: ", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + fmt.Printf("Gotify notification returned %d HTTP status code", resp.StatusCode) + } + + }() + return nil +} + +type gotifyMessage struct { + Message string `json:"message"` + Title string `json:"title"` + Priority int `json:"priority"` +} diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index f077c7f..2f25824 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -38,6 +38,8 @@ func NewNotifier(c *cobra.Command) *Notifier { tn = newSlackNotifier(c, acceptedLogLevels) case msTeamsType: tn = newMsTeamsNotifier(c, acceptedLogLevels) + case gotifyType: + tn = newGotifyNotifier(c, acceptedLogLevels) default: log.Fatalf("Unknown notification type %q", t) }