From be2376a4e1da4342fa9cbc96897aa2e77f0460d1 Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Mon, 23 Mar 2020 11:40:55 +0100 Subject: [PATCH 01/30] add shoutrrr --- docs/notifications.md | 21 +++++++++++++++++++++ go.mod | 1 + go.sum | 19 +++++++++++++++++++ internal/flags/flags.go | 8 +++++++- pkg/notifications/notifier.go | 2 ++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/notifications.md b/docs/notifications.md index b95e95e..6a1a9e9 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -8,6 +8,7 @@ The types of notifications to send are set by passing a comma-separated list of - `slack` to send notifications through a Slack webhook - `msteams` to send notifications via MSTeams webhook - `gotify` to send notifications via Gotify +- `shoutrrr` to send notifications via [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) > There is currently a [bug](https://github.com/spf13/viper/issues/380) in Viper, which prevents comma-separated slices to be used when using the environment variable. A workaround is available where we instead put quotes around the environment variable value and replace the commas with spaces, as `WATCHTOWER_NOTIFICATIONS="slack msteams"` @@ -115,3 +116,23 @@ docker run -d \ -e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN="SuperSecretToken" \ containrrr/watchtower ``` + +### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) + +To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set: + +- `--notification-shoutrrr-url` (env. `WATCHTOWER_NOTIFICATION_SHOUTRRR_URL`): The shoutrrr service URL to be used. + +Go to [https://github.com/containrrr/shoutrrr#service-urls](https://github.com/containrrr/shoutrrr#service-urls) to learn more about the different service URLs you can use. + +Example: + +```bash +docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATIONS=shoutrrr \ + -e WATCHTOWER_NOTIFICATION_SHOUTRRR_URL=discord://channel/token \ + -e WATCHTOWER_NOTIFICATION_SHOUTRRR_URL=slack://watchtower@token-a/token-b/token-c \ + containrrr/watchtower +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 2d2ced6..b6fad27 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 // indirect github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect + github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752 github.com/docker/cli v0.0.0-20190327152802-57b27434ea29 github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 diff --git a/go.sum b/go.sum index 230df62..f83bd71 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 h1:A7RURps5t4yDU0 github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752 h1:g7iPN6gYedYNetNT7pdSO2jyZWyg9f7OqIVB4wOcEh4= +github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -78,6 +80,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -108,6 +112,8 @@ github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLm github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -130,6 +136,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= +github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE= github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -161,6 +169,10 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -291,6 +303,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -298,6 +311,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= @@ -313,6 +327,9 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= @@ -345,6 +362,8 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gosrc.io/xmpp v0.1.1 h1:iMtE9W3fx254+4E6rI34AOPJDqWvpfQR6EYaVMzhJ4s= +gosrc.io/xmpp v0.1.1/go.mod h1:4JgaXzw4MnEv2sGltONtK3GMhj+h9gpQ7cO8nwbFJLU= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/flags/flags.go b/internal/flags/flags.go index a60d18f..4d54f92 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -121,7 +121,7 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { "notifications", "n", viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), - " notification types to send (valid: email, slack, msteams, gotify)") + " notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.StringP( "notifications-level", @@ -238,6 +238,12 @@ Should only be used for testing. "", viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), "The Gotify Application required to query the Gotify API") + + flags.StringArrayP( + "notification-shoutrrr-url", + "", + viper.GetStringSlice("WATCHTOWER_NOTIFICATION_SHOUTRRR_URL"), + "The shoutrrr URL to send notifications to") } // SetDefaults provides default values for environment variables diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 2f25824..a23a5ee 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -40,6 +40,8 @@ func NewNotifier(c *cobra.Command) *Notifier { tn = newMsTeamsNotifier(c, acceptedLogLevels) case gotifyType: tn = newGotifyNotifier(c, acceptedLogLevels) + case shoutrrrType: + tn = newShoutrrrNotifier(c, acceptedLogLevels) default: log.Fatalf("Unknown notification type %q", t) } From 2b21bd46bea45a7badb71c7f23ba1d0f10f2d15c Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Mon, 23 Mar 2020 13:52:04 +0100 Subject: [PATCH 02/30] add shoutrrr.go --- pkg/notifications/shoutrrr.go | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 pkg/notifications/shoutrrr.go diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go new file mode 100644 index 0000000..4c7d2ab --- /dev/null +++ b/pkg/notifications/shoutrrr.go @@ -0,0 +1,88 @@ +package notifications + +import ( + "fmt" + "github.com/containrrr/shoutrrr" + t "github.com/containrrr/watchtower/pkg/types" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const ( + shoutrrrType = "shoutrrr" +) + +// Implements Notifier, logrus.Hook +type shoutrrrTypeNotifier struct { + Urls []string + entries []*log.Entry + logLevels []log.Level +} + +func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() + + urls, _ := flags.GetStringArray("notification-shoutrrr-url") + + n := &shoutrrrTypeNotifier{ + Urls: urls, + logLevels: acceptedLogLevels, + } + + log.AddHook(n) + + return n +} + +func (e *shoutrrrTypeNotifier) buildMessage(entries []*log.Entry) string { + body := "" + for _, entry := range entries { + body += entry.Time.Format("2006-01-02 15:04:05") + " (" + entry.Level.String() + "): " + entry.Message + "\r\n" + // We don't use fields in watchtower, so don't bother sending them. + } + + return body +} + +func (e *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry) { + // Do the sending in a separate goroutine so we don't block the main process. + msg := e.buildMessage(entries) + go func() { + for _, url := range e.Urls { + err := shoutrrr.Send(url, msg) + if err != nil { + // Use fmt so it doesn't trigger another notification. + fmt.Println("Failed to send notification via shoutrrr (url="+url+"): ", err) + } + } + }() +} + +func (e *shoutrrrTypeNotifier) StartNotification() { + if e.entries == nil { + e.entries = make([]*log.Entry, 0, 10) + } +} + +func (e *shoutrrrTypeNotifier) SendNotification() { + if e.entries == nil || len(e.entries) <= 0 { + return + } + + e.sendEntries(e.entries) + e.entries = nil +} + +func (e *shoutrrrTypeNotifier) Levels() []log.Level { + return e.logLevels +} + +func (e *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { + if e.entries != nil { + e.entries = append(e.entries, entry) + } else { + // Log output generated outside a cycle is sent immediately. + e.sendEntries([]*log.Entry{entry}) + } + return nil +} From 59ce378a352f91d30f321e7a650c4ad9a4076cd5 Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Mon, 23 Mar 2020 13:53:30 +0100 Subject: [PATCH 03/30] Adjust flags --- docs/notifications.md | 6 +++--- internal/flags/flags.go | 4 ++-- pkg/notifications/shoutrrr.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/notifications.md b/docs/notifications.md index 6a1a9e9..32ada29 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -121,7 +121,7 @@ docker run -d \ To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set: -- `--notification-shoutrrr-url` (env. `WATCHTOWER_NOTIFICATION_SHOUTRRR_URL`): The shoutrrr service URL to be used. +- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. Go to [https://github.com/containrrr/shoutrrr#service-urls](https://github.com/containrrr/shoutrrr#service-urls) to learn more about the different service URLs you can use. @@ -132,7 +132,7 @@ docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ -e WATCHTOWER_NOTIFICATIONS=shoutrrr \ - -e WATCHTOWER_NOTIFICATION_SHOUTRRR_URL=discord://channel/token \ - -e WATCHTOWER_NOTIFICATION_SHOUTRRR_URL=slack://watchtower@token-a/token-b/token-c \ + -e WATCHTOWER_NOTIFICATION_URL=discord://channel/token \ + -e WATCHTOWER_NOTIFICATION_URL=slack://watchtower@token-a/token-b/token-c \ containrrr/watchtower ``` \ No newline at end of file diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 4d54f92..29c0913 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -240,9 +240,9 @@ Should only be used for testing. "The Gotify Application required to query the Gotify API") flags.StringArrayP( - "notification-shoutrrr-url", + "notification-url", "", - viper.GetStringSlice("WATCHTOWER_NOTIFICATION_SHOUTRRR_URL"), + viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), "The shoutrrr URL to send notifications to") } diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index 4c7d2ab..a7d786c 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -22,7 +22,7 @@ type shoutrrrTypeNotifier struct { func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { flags := c.PersistentFlags() - urls, _ := flags.GetStringArray("notification-shoutrrr-url") + urls, _ := flags.GetStringArray("notification-url") n := &shoutrrrTypeNotifier{ Urls: urls, From 5869bc52aa6be669d7d0b79bb21afb2f46bf8fff Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Mon, 23 Mar 2020 14:27:09 +0100 Subject: [PATCH 04/30] Use CreateSender instead of calling Send multiple times --- pkg/notifications/shoutrrr.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index a7d786c..869ce54 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -48,11 +48,13 @@ func (e *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry) { // Do the sending in a separate goroutine so we don't block the main process. msg := e.buildMessage(entries) go func() { - for _, url := range e.Urls { - err := shoutrrr.Send(url, msg) + router, _ := shoutrrr.CreateSender(e.Urls...) + errs := router.Send(msg, nil) + + for i, err := range errs { if err != nil { // Use fmt so it doesn't trigger another notification. - fmt.Println("Failed to send notification via shoutrrr (url="+url+"): ", err) + fmt.Println("Failed to send notification via shoutrrr (url="+e.Urls[i]+"): ", err) } } }() From b5df48279ca7e5083ab7a335d29fbf1f61a84780 Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Mon, 23 Mar 2020 14:34:36 +0100 Subject: [PATCH 05/30] Adjust documentation --- docs/notifications.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/notifications.md b/docs/notifications.md index 32ada29..c2a92a1 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -124,6 +124,7 @@ To send notifications via shoutrrr, the following command-line options, or their - `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. Go to [https://github.com/containrrr/shoutrrr#service-urls](https://github.com/containrrr/shoutrrr#service-urls) to learn more about the different service URLs you can use. +You can define multiple services by space separating the URLs. (See example below) Example: @@ -132,7 +133,6 @@ docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ -e WATCHTOWER_NOTIFICATIONS=shoutrrr \ - -e WATCHTOWER_NOTIFICATION_URL=discord://channel/token \ - -e WATCHTOWER_NOTIFICATION_URL=slack://watchtower@token-a/token-b/token-c \ + -e WATCHTOWER_NOTIFICATION_URL="discord://channel/token slack://watchtower@token-a/token-b/token-c" \ containrrr/watchtower ``` \ No newline at end of file From 480f4c8ccbe34acec873cead28b60d15edec9524 Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Mon, 23 Mar 2020 14:42:17 +0100 Subject: [PATCH 06/30] reuse router --- pkg/notifications/shoutrrr.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index 869ce54..108e3b6 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -3,6 +3,7 @@ package notifications import ( "fmt" "github.com/containrrr/shoutrrr" + "github.com/containrrr/shoutrrr/pkg/router" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -15,6 +16,7 @@ const ( // Implements Notifier, logrus.Hook type shoutrrrTypeNotifier struct { Urls []string + Router *router.ServiceRouter entries []*log.Entry logLevels []log.Level } @@ -23,9 +25,11 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti flags := c.PersistentFlags() urls, _ := flags.GetStringArray("notification-url") + r, _ := shoutrrr.CreateSender(urls...) n := &shoutrrrTypeNotifier{ Urls: urls, + Router: r, logLevels: acceptedLogLevels, } @@ -48,8 +52,7 @@ func (e *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry) { // Do the sending in a separate goroutine so we don't block the main process. msg := e.buildMessage(entries) go func() { - router, _ := shoutrrr.CreateSender(e.Urls...) - errs := router.Send(msg, nil) + errs := e.Router.Send(msg, nil) for i, err := range errs { if err != nil { From 205b1766b4cc861a8f9a91b2699919af2ee1aeaa Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Thu, 26 Mar 2020 16:58:57 +0100 Subject: [PATCH 07/30] fix #472 --- cmd/root.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index ee64e56..cf2d5c6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,7 +113,9 @@ func Run(c *cobra.Command, names []string) { runOnce, _ := c.PersistentFlags().GetBool("run-once") if runOnce { - log.Info("Running a one time update.") + if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { + log.Info("Running a one time update.") + } runUpdatesWithNotifications(filter) os.Exit(0) return From 0462c30bfb0c596a75b4f92ad4a61d89ba5c013b Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Fri, 27 Mar 2020 11:56:58 +0100 Subject: [PATCH 08/30] clarify container selection --- docs/container-selection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/container-selection.md b/docs/container-selection.md index 4c3312c..459d53b 100644 --- a/docs/container-selection.md +++ b/docs/container-selection.md @@ -12,7 +12,7 @@ Or, it can be specified as part of the `docker run` command line: docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage ``` -If you need to include only some containers, pass the `--label-enable` flag on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` for the containers you want to watch. +If you need to [include only containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCTOWER_LABEL_ENV` environment variable on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` for the containers you want to watch. ```docker LABEL com.centurylinklabs.watchtower.enable="true" @@ -22,4 +22,4 @@ Or, it can be specified as part of the `docker run` command line: ```bash docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage -``` \ No newline at end of file +``` From 7542a247d8521be7993c6819252f1c2525ac66d6 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Fri, 27 Mar 2020 11:57:14 +0100 Subject: [PATCH 09/30] Update container-selection.md --- docs/container-selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/container-selection.md b/docs/container-selection.md index 459d53b..7283ce9 100644 --- a/docs/container-selection.md +++ b/docs/container-selection.md @@ -12,7 +12,7 @@ Or, it can be specified as part of the `docker run` command line: docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage ``` -If you need to [include only containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCTOWER_LABEL_ENV` environment variable on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` for the containers you want to watch. +If you need to [include only containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCTOWER_LABEL_ENABLE` environment variable on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` for the containers you want to watch. ```docker LABEL com.centurylinklabs.watchtower.enable="true" From 638c697dece24205c45e76afc1204935f3dc710c Mon Sep 17 00:00:00 2001 From: sixcorners Date: Fri, 27 Mar 2020 07:57:04 -0500 Subject: [PATCH 10/30] Copy note about setting both interval and schedule --- docs/arguments.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/arguments.md b/docs/arguments.md index 7e55dc4..e55bd3b 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -119,7 +119,7 @@ Environment Variable: WATCHTOWER_REVIVE_STOPPED ``` ## Poll interval -Poll interval (in seconds). This value controls how frequently watchtower will poll for new images. +Poll interval (in seconds). This value controls how frequently watchtower will poll for new images. Either `--schedule` or a poll interval can be defined, but not both. ``` Argument: --interval, -i @@ -192,7 +192,8 @@ Environment Variable: WATCHTOWER_RUN_ONCE ``` ## Scheduling -[Cron expression](https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression could be defined, but not both. An example: `--schedule "0 0 4 * * *"` +[Cron expression](https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression +can be defined, but not both. An example: `--schedule "0 0 4 * * *"` ``` Argument: --schedule, -s From ce459635a536dd504e39bd538f0af209a9d2f8f9 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 27 Mar 2020 14:58:57 -0500 Subject: [PATCH 11/30] Update arguments.md Add clarity to --monitor-only, information on alternative timezone setting method. --- docs/arguments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/arguments.md b/docs/arguments.md index 7e55dc4..df2d09a 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -39,7 +39,7 @@ Environment Variable: N/A ## Time Zone Sets the time zone to be used by WatchTower's logs and the optional Cron scheduling argument (--schedule). If this environment variable is not set, Watchtower will use the default time zone: UTC. -To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. +To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezome can alternatively be set by volume mounting your hosts /etc/timezone file. `-v /etc/timezone:/etc/timezone:ro` ``` Argument: N/A @@ -139,7 +139,7 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE ``` ## Without updating containers -Will only monitor for new images, not update the containers. +Will only monitor for new images, not update the containers. Please note: Due to Docker API limitations the latest image will still be pulled from the registry. ``` Argument: --monitor-only From 52a8bd3bd26afef72073c5b09da567a18a7df0b8 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sat, 28 Mar 2020 18:49:23 +0100 Subject: [PATCH 12/30] Update arguments.md --- docs/arguments.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index df2d09a..c90c9ea 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -139,7 +139,11 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE ``` ## Without updating containers -Will only monitor for new images, not update the containers. Please note: Due to Docker API limitations the latest image will still be pulled from the registry. +Will only monitor for new images, not update the containers. + +> ### ⚠️ Please note +> +> Due to Docker API limitations the latest image will still be pulled from the registry. ``` Argument: --monitor-only From d001fbc48447bedbb328c4d3bd10969dc28abea6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2020 17:49:57 +0000 Subject: [PATCH 13/30] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 396a155..729630c 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Oliver Cervera

📖
Victor Moura

⚠️
Maximilian Brandau

💻 +
Andrew

📖 From 0c35e46c1b1ca54a481b3c8d6b84001b7e86b8c0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2020 17:49:58 +0000 Subject: [PATCH 14/30] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4c48400..7823e3d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -485,6 +485,15 @@ "contributions": [ "code" ] + }, + { + "login": "aneisch", + "name": "Andrew", + "avatar_url": "https://avatars1.githubusercontent.com/u/6991461?v=4", + "profile": "https://github.com/aneisch", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, From 1d1c630f7a79c59c0383a12649dc336389886d6e Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 25 Nov 2019 21:11:12 +0100 Subject: [PATCH 15/30] feat: add timeout override for pre-update lifecycle hook --- docs/lifecycle-hooks.md | 13 ++++++++++ go.sum | 2 ++ internal/actions/update.go | 3 +-- internal/flags/flags.go | 6 +++-- pkg/container/client.go | 50 +++++++++++++++++++++++++++++--------- pkg/container/container.go | 22 ++++++++++++++++- pkg/container/metadata.go | 17 +++++++------ 7 files changed, 89 insertions(+), 24 deletions(-) diff --git a/docs/lifecycle-hooks.md b/docs/lifecycle-hooks.md index 071726c..f8bc640 100644 --- a/docs/lifecycle-hooks.md +++ b/docs/lifecycle-hooks.md @@ -46,6 +46,19 @@ docker run -d \ --label=com.centurylinklabs.watchtower.lifecycle.post-check="/send-heartbeat.sh" \ ``` +### Timeouts +The timeout for all lifecycle commands is 60 seconds. After that, a timeout will +occur, forcing Watchtower to continue the update loop. + +#### Pre-update timeouts + +For the `pre-update` lifecycle command, it is possible to override this timeout to +allow the script to finish before forcefully killing it. This is done by adding the +label `com.centurylinklabs.watchtower.lifecycle.pre-update-timeout` followed by +the timeout expressed in minutes. + +If the label value is explicitly set to `0`, the timeout will be disabled. + ### Execution failure The failure of a command to execute, identified by an exit code different than diff --git a/go.sum b/go.sum index 230df62..9531de6 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,7 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 h1:34LfsqlE2kEvmGP9qbRoPvOWkmluYGzmlvWVTzwvT0A= github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= github.com/docker/docker-credential-helpers v0.6.1 h1:Dq4iIfcM7cNtddhLVWe9h4QDjsi4OER3Z8voPu/I52g= github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go v1.5.1-1 h1:hr4w35acWBPhGBXlzPoHpmZ/ygPjnmFVxGxxGnMyP7k= @@ -284,6 +285,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20191116160921-f9c825593386 h1:ktbWvQrW08Txdxno1PiDpSxPXG6ndGsfnJjRRtkM0LQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/actions/update.go b/internal/actions/update.go index 874e705..31ceafe 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -75,7 +75,6 @@ func stopStaleContainer(container container.Container, client container.Client, } if params.LifecycleHooks { lifecycle.ExecutePreUpdateCommand(client, container) - } if err := client.StopContainer(container, params.Timeout); err != nil { @@ -140,4 +139,4 @@ func checkDependencies(containers []container.Container) { } } } -} +} \ No newline at end of file diff --git a/internal/flags/flags.go b/internal/flags/flags.go index a60d18f..039b574 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -30,12 +30,14 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { viper.GetInt("WATCHTOWER_POLL_INTERVAL"), "poll interval (in seconds)") - flags.StringP("schedule", + flags.StringP( + "schedule", "s", viper.GetString("WATCHTOWER_SCHEDULE"), "the cron expression which defines when to update") - flags.DurationP("stop-timeout", + flags.DurationP( + "stop-timeout", "t", viper.GetDuration("WATCHTOWER_TIMEOUT"), "timeout before a container is forcefully stopped") diff --git a/pkg/container/client.go b/pkg/container/client.go index 607b84c..a74bfba 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -29,9 +29,8 @@ type Client interface { StartContainer(Container) (string, error) RenameContainer(Container, string) error IsContainerStale(Container) (bool, error) - ExecuteCommand(containerID string, command string) error + ExecuteCommand(containerID string, command string, timeout int) error RemoveImageByID(string) error - } // NewClient returns a new Client instance which can be used to interact with @@ -301,7 +300,7 @@ func (client dockerClient) RemoveImageByID(id string) error { return err } -func (client dockerClient) ExecuteCommand(containerID string, command string) error { +func (client dockerClient) ExecuteCommand(containerID string, command string, timeout int) error { bg := context.Background() // Create the exec @@ -331,7 +330,7 @@ func (client dockerClient) ExecuteCommand(containerID string, command string) er return err } - var execOutput string + var output string if attachErr == nil { defer response.Close() var writer bytes.Buffer @@ -339,26 +338,56 @@ func (client dockerClient) ExecuteCommand(containerID string, command string) er if err != nil { log.Error(err) } else if written > 0 { - execOutput = strings.TrimSpace(writer.String()) + output = strings.TrimSpace(writer.String()) } } // Inspect the exec to get the exit code and print a message if the // exit code is not success. - execInspect, err := client.api.ContainerExecInspect(bg, exec.ID) + err = client.waitForExecOrTimeout(bg, exec.ID, output, timeout) if err != nil { return err } - if execInspect.ExitCode > 0 { - log.Errorf("Command exited with code %v.", execInspect.ExitCode) - log.Error(execOutput) + return nil +} + +func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, execOutput string, timeout int) error { + var ctx context.Context + var cancel context.CancelFunc + + if timeout > 0 { + ctx, cancel = context.WithTimeout(bg, time.Duration(timeout)*time.Minute) + defer cancel() } else { + ctx = bg + } + + for { + execInspect, err := client.api.ContainerExecInspect(ctx, ID) + + log.WithFields(log.Fields{ + "exit-code": execInspect.ExitCode, + "exec-id": execInspect.ExecID, + "running": execInspect.Running, + }).Debug("Awaiting timeout or completion") + + if err != nil { + return err + } + if execInspect.Running == true { + time.Sleep(1 * time.Second) + continue + } if len(execOutput) > 0 { log.Infof("Command output:\n%v", execOutput) } + if execInspect.ExitCode > 0 { + log.Errorf("Command exited with code %v.", execInspect.ExitCode) + log.Error(execOutput) + } + break } - return nil } @@ -377,7 +406,6 @@ func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Durat return nil } } - time.Sleep(1 * time.Second) } } diff --git a/pkg/container/container.go b/pkg/container/container.go index f88ff91..32e5a31 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -2,10 +2,11 @@ package container import ( "fmt" - "github.com/containrrr/watchtower/internal/util" "strconv" "strings" + "github.com/containrrr/watchtower/internal/util" + "github.com/docker/docker/api/types" dockercontainer "github.com/docker/docker/api/types/container" ) @@ -118,6 +119,25 @@ func (c Container) IsWatchtower() bool { return ContainsWatchtowerLabel(c.containerInfo.Config.Labels) } +// PreUpdateTimeout checks whether a container has a specific timeout set +// for how long the pre-update command is allowed to run. This value is expressed +// either as an integer, in minutes, or as "off" which will allow the command/script +// to run indefinitely. Users should be cautious with the off option, as that +// could result in watchtower waiting forever. +func (c Container) PreUpdateTimeout() int { + var minutes int + var err error + + val := c.getLabelValueOrEmpty(preUpdateTimeoutLabel) + + minutes, err = strconv.Atoi(val) + if err != nil || val == "" { + return 1 + } + + return minutes +} + // StopSignal returns the custom stop signal (if any) that is encoded in the // container's metadata. If the container has not specified a custom stop // signal, the empty string "" is returned. diff --git a/pkg/container/metadata.go b/pkg/container/metadata.go index 0e04350..fe5a055 100644 --- a/pkg/container/metadata.go +++ b/pkg/container/metadata.go @@ -1,14 +1,15 @@ package container const ( - watchtowerLabel = "com.centurylinklabs.watchtower" - signalLabel = "com.centurylinklabs.watchtower.stop-signal" - enableLabel = "com.centurylinklabs.watchtower.enable" - zodiacLabel = "com.centurylinklabs.zodiac.original-image" - preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check" - postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check" - preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update" - postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update" + watchtowerLabel = "com.centurylinklabs.watchtower" + signalLabel = "com.centurylinklabs.watchtower.stop-signal" + enableLabel = "com.centurylinklabs.watchtower.enable" + zodiacLabel = "com.centurylinklabs.zodiac.original-image" + preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check" + postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check" + preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update" + postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update" + preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout" ) // GetLifecyclePreCheckCommand returns the pre-check command set in the container metadata or an empty string From 1d3ffc728daa522d42cb09a0d9ce3ae3e4367a66 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 25 Nov 2019 21:19:38 +0100 Subject: [PATCH 16/30] fix: update mock client for tests --- internal/actions/actions_suite_test.go | 59 ++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/internal/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go index 2c9b0c8..b633b6a 100644 --- a/internal/actions/actions_suite_test.go +++ b/internal/actions/actions_suite_test.go @@ -132,3 +132,62 @@ var _ = Describe("the actions package", func() { }) }) +func createMockContainer(id string, name string, image string, created time.Time) container.Container { + content := types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: id, + Image: image, + Name: name, + Created: created.String(), + }, + } + return *container.NewContainer(&content, nil) +} + +type mockClient struct { + TestData *TestData + api cli.CommonAPIClient + pullImages bool + removeVolumes bool +} + +type TestData struct { + TriedToRemoveImage bool + NameOfContainerToKeep string + Containers []container.Container +} + +func (client mockClient) ListContainers(f t.Filter) ([]container.Container, error) { + return client.TestData.Containers, nil +} + +func (client mockClient) StopContainer(c container.Container, d time.Duration) error { + if c.Name() == client.TestData.NameOfContainerToKeep { + return errors.New("tried to stop the instance we want to keep") + } + return nil +} +func (client mockClient) StartContainer(c container.Container) (string, error) { + panic("Not implemented") +} + +func (client mockClient) RenameContainer(c container.Container, s string) error { + panic("Not implemented") +} + +func (client mockClient) RemoveImage(c container.Container) error { + client.TestData.TriedToRemoveImage = true + return nil +} + +func (client mockClient) GetContainer(containerID string) (container.Container, error) { + return container.Container{}, nil +} + +func (client mockClient) ExecuteCommand(containerID string, command string, timeout int) error { + return nil +} + +func (client mockClient) IsContainerStale(c container.Container) (bool, error) { + panic("Not implemented") +} From fac26dfc721e51cce7a49e6e32e3bd07ba91d07a Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Fri, 27 Dec 2019 12:05:56 +0100 Subject: [PATCH 17/30] fix: improve logging --- pkg/notifications/notifier.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 2f25824..20e8337 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -27,8 +27,10 @@ func NewNotifier(c *cobra.Command) *Notifier { acceptedLogLevels := slackrus.LevelThreshold(logLevel) // Parse types and create notifiers. - types, _ := f.GetStringSlice("notifications") - + types, err := f.GetStringSlice("notifications") + if err != nil { + log.WithField("could not read notifications argument", log.Fields{ "Error": err }).Fatal() + } for _, t := range types { var tn ty.Notifier switch t { From c1a0da9a9d5eb6587c3a5b0020b6c7113e18fade Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Fri, 3 Jan 2020 18:51:32 +0100 Subject: [PATCH 18/30] feature/367 fix: skip container if pre-update command fails --- internal/actions/update.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/actions/update.go b/internal/actions/update.go index 31ceafe..1694c59 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -74,9 +74,13 @@ func stopStaleContainer(container container.Container, client container.Client, return } if params.LifecycleHooks { - lifecycle.ExecutePreUpdateCommand(client, container) + if err := lifecycle.ExecutePreUpdateCommand(client, container); err != nil { + log.Error(err) + log.Info("Skipping container as the pre-update command failed") + return + } } - + if err := client.StopContainer(container, params.Timeout); err != nil { log.Error(err) } @@ -139,4 +143,4 @@ func checkDependencies(containers []container.Container) { } } } -} \ No newline at end of file +} From 98c60d744143b9dc245ddfdc14270b22cb739b73 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sat, 28 Mar 2020 19:48:04 +0100 Subject: [PATCH 19/30] fix some errors and clean up --- internal/actions/actions_suite_test.go | 51 +------------------------- internal/actions/mocks/client.go | 2 +- pkg/container/container.go | 4 +- pkg/lifecycle/lifecycle.go | 16 ++++---- 4 files changed, 12 insertions(+), 61 deletions(-) diff --git a/internal/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go index b633b6a..5bfbcdd 100644 --- a/internal/actions/actions_suite_test.go +++ b/internal/actions/actions_suite_test.go @@ -9,6 +9,7 @@ import ( "github.com/containrrr/watchtower/pkg/container/mocks" cli "github.com/docker/docker/client" + "github.com/docker/docker/api/types" . "github.com/containrrr/watchtower/internal/actions/mocks" . "github.com/onsi/ginkgo" @@ -142,52 +143,4 @@ func createMockContainer(id string, name string, image string, created time.Time }, } return *container.NewContainer(&content, nil) -} - -type mockClient struct { - TestData *TestData - api cli.CommonAPIClient - pullImages bool - removeVolumes bool -} - -type TestData struct { - TriedToRemoveImage bool - NameOfContainerToKeep string - Containers []container.Container -} - -func (client mockClient) ListContainers(f t.Filter) ([]container.Container, error) { - return client.TestData.Containers, nil -} - -func (client mockClient) StopContainer(c container.Container, d time.Duration) error { - if c.Name() == client.TestData.NameOfContainerToKeep { - return errors.New("tried to stop the instance we want to keep") - } - return nil -} -func (client mockClient) StartContainer(c container.Container) (string, error) { - panic("Not implemented") -} - -func (client mockClient) RenameContainer(c container.Container, s string) error { - panic("Not implemented") -} - -func (client mockClient) RemoveImage(c container.Container) error { - client.TestData.TriedToRemoveImage = true - return nil -} - -func (client mockClient) GetContainer(containerID string) (container.Container, error) { - return container.Container{}, nil -} - -func (client mockClient) ExecuteCommand(containerID string, command string, timeout int) error { - return nil -} - -func (client mockClient) IsContainerStale(c container.Container) (bool, error) { - panic("Not implemented") -} +} \ No newline at end of file diff --git a/internal/actions/mocks/client.go b/internal/actions/mocks/client.go index dad2506..2145484 100644 --- a/internal/actions/mocks/client.go +++ b/internal/actions/mocks/client.go @@ -73,7 +73,7 @@ func (client MockClient) GetContainer(containerID string) (container.Container, } // ExecuteCommand is a mock method -func (client MockClient) ExecuteCommand(containerID string, command string) error { +func (client MockClient) ExecuteCommand(containerID string, command string, timeout int) error { return nil } diff --git a/pkg/container/container.go b/pkg/container/container.go index 32e5a31..fb495fe 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -121,8 +121,8 @@ func (c Container) IsWatchtower() bool { // PreUpdateTimeout checks whether a container has a specific timeout set // for how long the pre-update command is allowed to run. This value is expressed -// either as an integer, in minutes, or as "off" which will allow the command/script -// to run indefinitely. Users should be cautious with the off option, as that +// either as an integer, in minutes, or as 0 which will allow the command/script +// to run indefinitely. Users should be cautious with the 0 option, as that // could result in watchtower waiting forever. func (c Container) PreUpdateTimeout() int { var minutes int diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go index 9823f9d..9311355 100644 --- a/pkg/lifecycle/lifecycle.go +++ b/pkg/lifecycle/lifecycle.go @@ -37,7 +37,7 @@ func ExecutePreCheckCommand(client container.Client, container container.Contain } log.Info("Executing pre-check command.") - if err := client.ExecuteCommand(container.ID(), command); err != nil { + if err := client.ExecuteCommand(container.ID(), command, 1); err != nil { log.Error(err) } } @@ -51,24 +51,22 @@ func ExecutePostCheckCommand(client container.Client, container container.Contai } log.Info("Executing post-check command.") - if err := client.ExecuteCommand(container.ID(), command); err != nil { + if err := client.ExecuteCommand(container.ID(), command, 1); err != nil { log.Error(err) } } // ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container. -func ExecutePreUpdateCommand(client container.Client, container container.Container) { - +func ExecutePreUpdateCommand(client container.Client, container container.Container) error { + timeout := container.PreUpdateTimeout() command := container.GetLifecyclePreUpdateCommand() if len(command) == 0 { log.Debug("No pre-update command supplied. Skipping") - return + return nil } log.Info("Executing pre-update command.") - if err := client.ExecuteCommand(container.ID(), command); err != nil { - log.Error(err) - } + return client.ExecuteCommand(container.ID(), command, timeout) } // ExecutePostUpdateCommand tries to run the post-update lifecycle hook for a single container. @@ -86,7 +84,7 @@ func ExecutePostUpdateCommand(client container.Client, newContainerID string) { } log.Info("Executing post-update command.") - if err := client.ExecuteCommand(newContainerID, command); err != nil { + if err := client.ExecuteCommand(newContainerID, command, 1); err != nil { log.Error(err) } } From 0e131e38bd39b44e74b2416ae29239e55ae9fcc2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2020 18:52:25 +0000 Subject: [PATCH 20/30] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 729630c..a5025e3 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Victor Moura

⚠️
Maximilian Brandau

💻
Andrew

📖 +
sixcorners

📖 From 6590ef248f9dd9880a7800a13c2dcddbe06fe22a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2020 18:52:26 +0000 Subject: [PATCH 21/30] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7823e3d..f438596 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -494,6 +494,15 @@ "contributions": [ "doc" ] + }, + { + "login": "sixcorners", + "name": "sixcorners", + "avatar_url": "https://avatars0.githubusercontent.com/u/585501?v=4", + "profile": "https://github.com/sixcorners", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, From 90ef4022f7c25386c53ce7e687aeb19411669039 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sat, 28 Mar 2020 20:05:59 +0100 Subject: [PATCH 22/30] add additional note on gcloud credentials --- docs/private-registries.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/private-registries.md b/docs/private-registries.md index 13e7618..3606c2a 100644 --- a/docs/private-registries.md +++ b/docs/private-registries.md @@ -1,12 +1,17 @@ -Watchtower supports private Docker image registries. In many cases, accessing a private registry requires a valid username and password (i.e., _credentials_). In order to operate in such an environment, watchtower needs to know the credentials to access the registry. +Watchtower supports private Docker image registries. In many cases, accessing a private registry +requires a valid username and password (i.e., _credentials_). In order to operate in such an +environment, watchtower needs to know the credentials to access the registry. -The credentials can be provided to watchtower in a configuration file called `config.json`. There are two ways to generate this configuration file: +The credentials can be provided to watchtower in a configuration file called `config.json`. +There are two ways to generate this configuration file: * The configuration file can be created manually. * Call `docker login ` and share the resulting configuration file. ### Create the configuration file manually -Create a new configuration file with the following syntax and a base64 encoded username and password `auth` string: +Create a new configuration file with the following syntax and a base64 encoded username and +password `auth` string: + ```json { "auths": { @@ -17,27 +22,40 @@ Create a new configuration file with the following syntax and a base64 encoded u } ``` -`` needs to be replaced by the name of your private registry (e.g., `my-private-registry.example.org`) +`` needs to be replaced by the name of your private registry +(e.g., `my-private-registry.example.org`) The required `auth` string can be generated as follows: ```bash echo -n 'username:password' | base64 ``` -When the watchtower Docker container is started, the created configuration file (`/config.json` in this example) needs to be passed to the container: +> ### ℹ️ Username and Password for GCloud +> +> For gcloud, we'll use `__json_key` as our username and the content +> of `gcloudauth.json` as the password. + +When the watchtower Docker container is started, the created configuration file +(`/config.json` in this example) needs to be passed to the container: + ```bash docker run [...] -v /config.json:/config.json containrrr/watchtower ``` ### Share the Docker configuration file -To pull an image from a private registry, `docker login` needs to be called first, to get access to the registry. The provided credentials are stored in a configuration file called `/.docker/config.json`. This configuration file can be directly used by watchtower. In this case, the creation of an additional configuration file is not necessary. +To pull an image from a private registry, `docker login` needs to be called first, to get access +to the registry. The provided credentials are stored in a configuration file called `/.docker/config.json`. +This configuration file can be directly used by watchtower. In this case, the creation of an +additional configuration file is not necessary. When the Docker container is started, pass the configuration file to watchtower: + ```bash docker run [...] -v /.docker/config.json:/config.json containrrr/watchtower ``` When creating the watchtower container via docker-compose, use the following lines: + ```yaml version: "3" [...] From 21e8799ce3c6276490aee778bf129ceaf73e46e9 Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Tue, 31 Mar 2020 11:29:49 +0200 Subject: [PATCH 23/30] Update documentation --- docs/notifications.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/notifications.md b/docs/notifications.md index c2a92a1..3698866 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -123,7 +123,7 @@ To send notifications via shoutrrr, the following command-line options, or their - `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. -Go to [https://github.com/containrrr/shoutrrr#service-urls](https://github.com/containrrr/shoutrrr#service-urls) to learn more about the different service URLs you can use. +Go to [containrrr.github.io/shoutrrr/services/overview](https://containrrr.github.io/shoutrrr/services/overview) to learn more about the different service URLs you can use. You can define multiple services by space separating the URLs. (See example below) Example: @@ -133,6 +133,6 @@ docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ -e WATCHTOWER_NOTIFICATIONS=shoutrrr \ - -e WATCHTOWER_NOTIFICATION_URL="discord://channel/token slack://watchtower@token-a/token-b/token-c" \ + -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ containrrr/watchtower ``` \ No newline at end of file From 2381c279f40afcd34a843300748d5ffcdbdb7c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Wed, 1 Apr 2020 19:38:35 +0200 Subject: [PATCH 24/30] docs: update cron docs link Update the robfig/cron documentation link which currently points to the v3 version and not the v1 version that watchtower uses --- docs/arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index 6aabb85..3a5fab5 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -196,7 +196,7 @@ Environment Variable: WATCHTOWER_RUN_ONCE ``` ## Scheduling -[Cron expression](https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression +[Cron expression](https://pkg.go.dev/github.com/robfig/cron@v1.2.0?tab=doc#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression can be defined, but not both. An example: `--schedule "0 0 4 * * *"` ``` From c49cc506c11ebe149ed0a9c247c0e69f66dfe588 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2020 19:26:53 +0000 Subject: [PATCH 25/30] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a5025e3..ed78058 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Maximilian Brandau

💻
Andrew

📖
sixcorners

📖 +
nils måsén

📖 From 8cbc9ebafb5baa8c5ea7132dbd916c52c01e0ff2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2020 19:26:54 +0000 Subject: [PATCH 26/30] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index f438596..7965cc4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -503,6 +503,15 @@ "contributions": [ "doc" ] + }, + { + "login": "piksel", + "name": "nils måsén", + "avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4", + "profile": "https://piksel.se", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, From a8d453b4c7297d0bf5de58a2a0386945445a08a2 Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Sun, 5 Apr 2020 11:10:47 +0200 Subject: [PATCH 27/30] update shoutrrr --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b6fad27..9d0138d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 // indirect github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect - github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752 + github.com/containrrr/shoutrrr v0.0.0-20200404203330-157bd996ea13 github.com/docker/cli v0.0.0-20190327152802-57b27434ea29 github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 diff --git a/go.sum b/go.sum index f83bd71..a9aff4f 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882b github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752 h1:g7iPN6gYedYNetNT7pdSO2jyZWyg9f7OqIVB4wOcEh4= github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc= +github.com/containrrr/shoutrrr v0.0.0-20200404203330-157bd996ea13 h1:5KIwcRac24xehTL/xrhXNIiI9JnV2Mbfl52OgGbloIM= +github.com/containrrr/shoutrrr v0.0.0-20200404203330-157bd996ea13/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= From d17e2887fba066916b0dce7ddfa01eab374cc831 Mon Sep 17 00:00:00 2001 From: Maximilian Brandau Date: Sun, 5 Apr 2020 11:51:44 +0200 Subject: [PATCH 28/30] remove old shoutrrr version --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index a9aff4f..97a634b 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,6 @@ github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 h1:A7RURps5t4yDU0 github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752 h1:g7iPN6gYedYNetNT7pdSO2jyZWyg9f7OqIVB4wOcEh4= -github.com/containrrr/shoutrrr v0.0.0-20200308125025-1981b9ef7752/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc= github.com/containrrr/shoutrrr v0.0.0-20200404203330-157bd996ea13 h1:5KIwcRac24xehTL/xrhXNIiI9JnV2Mbfl52OgGbloIM= github.com/containrrr/shoutrrr v0.0.0-20200404203330-157bd996ea13/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= From 1d296a3e1d22cfe3abb89785ec90e3114b894151 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2020 15:47:34 +0000 Subject: [PATCH 29/30] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ed78058..29f5dab 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Andrew

📖
sixcorners

📖
nils måsén

📖 +
Arne Jørgensen

⚠️ 👀 From 928da2ba546002264b71e7cfae2f56f4ba90dfe7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2020 15:47:35 +0000 Subject: [PATCH 30/30] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7965cc4..da468d1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -512,6 +512,16 @@ "contributions": [ "doc" ] + }, + { + "login": "arnested", + "name": "Arne Jørgensen", + "avatar_url": "https://avatars2.githubusercontent.com/u/190005?v=4", + "profile": "https://arnested.dk", + "contributions": [ + "test", + "review" + ] } ], "contributorsPerLine": 7,