Merge branch 'master' into all-contributors/add-mbrandau

pull/494/head
Simon Aronsson 4 years ago committed by GitHub
commit 1d55d229a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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,

@ -137,6 +137,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/auanasgheps"><img src="https://avatars2.githubusercontent.com/u/20586878?v=4" width="100px;" alt=""/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=auanasgheps" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/victorcmoura"><img src="https://avatars1.githubusercontent.com/u/26290053?v=4" width="100px;" alt=""/><br /><sub><b>Victor Moura</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4" width="100px;" alt=""/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4" width="100px;" alt=""/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4" width="100px;" alt=""/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td>
<td align="center"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4" width="100px;" alt=""/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a></td>
<td align="center"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4" width="100px;" alt=""/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td>
</tr>
</table>

@ -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

@ -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

@ -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
```
```

@ -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

@ -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
```

@ -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 <REGISTRY_NAME>` 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
}
```
`<REGISTRY_NAME>` needs to be replaced by the name of your private registry (e.g., `my-private-registry.example.org`)
`<REGISTRY_NAME>` 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 (`<PATH>/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
(`<PATH>/config.json` in this example) needs to be passed to the container:
```bash
docker run [...] -v <PATH>/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 `<PATH_TO_HOME_DIR>/.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 `<PATH_TO_HOME_DIR>/.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 <PATH_TO_HOME_DIR>/.docker/config.json:/config.json containrrr/watchtower
```
When creating the watchtower container via docker-compose, use the following lines:
```yaml
version: "3"
[...]

@ -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

@ -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=

@ -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)
}

@ -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
}

@ -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)
}

@ -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

@ -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)
}
}

@ -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.

@ -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

@ -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)
}
}

@ -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)
}

@ -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
}
Loading…
Cancel
Save