diff --git a/.all-contributorsrc b/.all-contributorsrc
index 8147f89..41f5863 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -486,6 +486,43 @@
"code",
"test"
]
+ },
+ {
+ "login": "aneisch",
+ "name": "Andrew",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/6991461?v=4",
+ "profile": "https://github.com/aneisch",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "sixcorners",
+ "name": "sixcorners",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/585501?v=4",
+ "profile": "https://github.com/sixcorners",
+ "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"
+ ]
+ },
+ {
+ "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,
diff --git a/README.md b/README.md
index e7127ce..33773bb 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Oliver Cervera 📖 |
Victor Moura ⚠️ |
Maximilian Brandau 💻 ⚠️ |
+ Andrew 📖 |
+ sixcorners 📖 |
+ nils måsén 📖 |
+ Arne Jørgensen ⚠️ 👀 |
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
diff --git a/docs/arguments.md b/docs/arguments.md
index 7e55dc4..3a5fab5 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
@@ -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
@@ -139,7 +139,11 @@ 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
@@ -192,7 +196,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://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 * * *"`
```
Argument: --schedule, -s
diff --git a/docs/container-selection.md b/docs/container-selection.md
index 4c3312c..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 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_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"
@@ -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
+```
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/docs/notifications.md b/docs/notifications.md
index bcb44ea..e60d765 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"`
@@ -116,3 +117,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-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used.
+
+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:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -e WATCHTOWER_NOTIFICATIONS=shoutrrr \
+ -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
+ containrrr/watchtower
+```
\ No newline at end of file
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"
[...]
diff --git a/go.mod b/go.mod
index 2d2ced6..9d0138d 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-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 230df62..ceec403 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-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=
@@ -61,6 +63,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=
@@ -78,6 +81,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 +113,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 +137,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 +170,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=
@@ -284,6 +297,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=
@@ -291,6 +305,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 +313,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 +329,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 +364,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/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go
index 2c9b0c8..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"
@@ -132,3 +133,14 @@ 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)
+}
\ 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/internal/actions/update.go b/internal/actions/update.go
index 874e705..1694c59 100644
--- a/internal/actions/update.go
+++ b/internal/actions/update.go
@@ -74,10 +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)
}
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index a60d18f..d8c7dff 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")
@@ -121,7 +123,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 +240,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-url",
+ "",
+ viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"),
+ "The shoutrrr URL to send notifications to")
}
// SetDefaults provides default values for environment variables
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..fb495fe 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 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
+ 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
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)
}
}
diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go
index 2f25824..fc6a310 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 {
@@ -40,6 +42,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)
}
diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go
new file mode 100644
index 0000000..108e3b6
--- /dev/null
+++ b/pkg/notifications/shoutrrr.go
@@ -0,0 +1,93 @@
+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"
+)
+
+const (
+ shoutrrrType = "shoutrrr"
+)
+
+// Implements Notifier, logrus.Hook
+type shoutrrrTypeNotifier struct {
+ Urls []string
+ Router *router.ServiceRouter
+ entries []*log.Entry
+ logLevels []log.Level
+}
+
+func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
+ flags := c.PersistentFlags()
+
+ urls, _ := flags.GetStringArray("notification-url")
+ r, _ := shoutrrr.CreateSender(urls...)
+
+ n := &shoutrrrTypeNotifier{
+ Urls: urls,
+ Router: r,
+ 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() {
+ errs := e.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="+e.Urls[i]+"): ", 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
+}