diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 0000000..a02444c
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,377 @@
+{
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": false,
+ "contributors": [
+ {
+ "login": "Codelica",
+ "name": "James",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/386101?v=4",
+ "profile": "http://codelica.com",
+ "contributions": [
+ "test",
+ "ideas"
+ ]
+ },
+ {
+ "login": "KopfKrieg",
+ "name": "Florian",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/5047813?v=4",
+ "profile": "https://kopfkrieg.org",
+ "contributions": [
+ "review",
+ "doc"
+ ]
+ },
+ {
+ "login": "bdehamer",
+ "name": "Brian DeHamer",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/398027?v=4",
+ "profile": "https://github.com/bdehamer",
+ "contributions": [
+ "code",
+ "maintenance"
+ ]
+ },
+ {
+ "login": "rosscado",
+ "name": "Ross Cadogan",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/16578183?v=4",
+ "profile": "https://github.com/rosscado",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "stffabi",
+ "name": "stffabi",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/9464631?v=4",
+ "profile": "https://github.com/stffabi",
+ "contributions": [
+ "code",
+ "maintenance"
+ ]
+ },
+ {
+ "login": "ATCUSA",
+ "name": "Austin",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/3581228?v=4",
+ "profile": "https://github.com/ATCUSA",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "davidgardner11",
+ "name": "David Gardner",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/6181487?v=4",
+ "profile": "https://labs.ctl.io",
+ "contributions": [
+ "review",
+ "doc"
+ ]
+ },
+ {
+ "login": "dolanor",
+ "name": "Tanguy β§ Herrmann",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/928722?v=4",
+ "profile": "https://github.com/dolanor",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "rdamazio",
+ "name": "Rodrigo Damazio Bovendorp",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/997641?v=4",
+ "profile": "https://github.com/rdamazio",
+ "contributions": [
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "thelamer",
+ "name": "Ryan Kuba",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/1852688?v=4",
+ "profile": "https://www.taisun.io/",
+ "contributions": [
+ "infra"
+ ]
+ },
+ {
+ "login": "cnrmck",
+ "name": "cnrmck",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/22061955?v=4",
+ "profile": "https://github.com/cnrmck",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "haswalt",
+ "name": "Harry Walter",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/338588?v=4",
+ "profile": "http://harrywalter.co.uk",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Robotex",
+ "name": "Robotex",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/74515?v=4",
+ "profile": "http://projectsperanza.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "ubergesundheit",
+ "name": "Gerald Pape",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1494211?v=4",
+ "profile": "http://geraldpape.io",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "fomk",
+ "name": "fomk",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/17636183?v=4",
+ "profile": "https://github.com/fomk",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "svengo",
+ "name": "Sven Gottwald",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/2502366?v=4",
+ "profile": "https://github.com/svengo",
+ "contributions": [
+ "infra"
+ ]
+ },
+ {
+ "login": "techknowlogick",
+ "name": "techknowlogick",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/164197?v=4",
+ "profile": "https://liberapay.com/techknowlogick/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "waja",
+ "name": "waja",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/1449568?v=4",
+ "profile": "http://log.c5t.org/about/",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "salbertson",
+ "name": "Scott Albertson",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/154463?v=4",
+ "profile": "http://scottalbertson.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "huddlesj",
+ "name": "Jason Huddleston",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/11966535?v=4",
+ "profile": "https://github.com/huddlesj",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "napstr",
+ "name": "Napster",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/6048348?v=4",
+ "profile": "https://npstr.space/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "darknode",
+ "name": "Maxim",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/809429?v=4",
+ "profile": "https://github.com/darknode",
+ "contributions": [
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "mxschmitt",
+ "name": "Max Schmitt",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/17984549?v=4",
+ "profile": "https://schmitt.cat",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "cron410",
+ "name": "cron410",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/3082899?v=4",
+ "profile": "https://github.com/cron410",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "Cardoso222",
+ "name": "Paulo Henrique",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/7026517?v=4",
+ "profile": "https://github.com/Cardoso222",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "belak",
+ "name": "Kaleb Elwert",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/107097?v=4",
+ "profile": "https://coded.io",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "wmbutler",
+ "name": "Bill Butler",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/1254810?v=4",
+ "profile": "https://github.com/wmbutler",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "mariotacke",
+ "name": "Mario Tacke",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/4942019?v=4",
+ "profile": "https://www.mariotacke.io",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mrw34",
+ "name": "Mark Woodbridge",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/1101318?v=4",
+ "profile": "https://markwoodbridge.com",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "simskij",
+ "name": "Simon Aronsson",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
+ "profile": "http://www.arcticbit.se",
+ "contributions": [
+ "code",
+ "maintenance",
+ "review"
+ ]
+ },
+ {
+ "login": "Ansem93",
+ "name": "Ansem93",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/6626218?v=4",
+ "profile": "https://github.com/Ansem93",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "lukapeschke",
+ "name": "Luka Peschke",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/17085536?v=4",
+ "profile": "https://github.com/lukapeschke",
+ "contributions": [
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "zoispag",
+ "name": "Zois Pagoulatos",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/21138205?v=4",
+ "profile": "https://github.com/zoispag",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "alexandremenif",
+ "name": "Alexandre Menif",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/16152103?v=4",
+ "profile": "https://alexandre.menif.name",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "chugunov",
+ "name": "Andrey",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/4140479?v=4",
+ "profile": "https://github.com/chugunov",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "noplanman",
+ "name": "Armando LΓΌscher",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/9423417?v=4",
+ "profile": "https://noplanman.ch",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "rjbudke",
+ "name": "Ryan Budke",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/273485?v=4",
+ "profile": "https://github.com/rjbudke",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "kaloyan-raev",
+ "name": "Kaloyan Raev",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/468091?v=4",
+ "profile": "http://kaloyan.raev.name",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "sixth",
+ "name": "sixth",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/11591445?v=4",
+ "profile": "https://github.com/sixth",
+ "contributions": [
+ "doc"
+ ]
+ }
+ ],
+ "contributorsPerLine": 7,
+ "projectName": "watchtower",
+ "projectOwner": "containrrr",
+ "repoType": "github",
+ "repoHost": "https://github.com",
+ "commitConvention": "none"
+}
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8dff539..c6b93d1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,6 +1,10 @@
version: 2.1
executors:
+ py:
+ docker:
+ - image: circleci/python:latest
+ working_directory: ~/repo
go:
docker:
- image: circleci/golang:latest
@@ -32,9 +36,18 @@ workflows:
only: /.*/
tags:
only: /.*/
+ # - integration_testing:
+ # requires:
+ # - checkout
+ # filters:
+ # branches:
+ # only: /.*/
+ # tags:
+ # only: /.*/
- build:
requires:
- testing
+ # - integration_testing
- linting
filters:
branches:
@@ -50,6 +63,15 @@ workflows:
ignore: /.*/
tags:
only: /^v[0-9]+(\.[0-9]+)*$/
+ - publish-docs:
+ requires:
+ - testing
+ - linting
+ filters:
+ branches:
+ ignore: /.*/
+ tags:
+ only: /^v[0-9]+(\.[0-9]+)*$/
jobs:
checkout:
executor: go
@@ -77,6 +99,14 @@ jobs:
- run: go get -u github.com/haya14busa/goverage
- run: goverage -v -coverprofile=coverage.out ./...
- run: godacov -t $CODACY_TOKEN -r ./coverage.out -c $CIRCLE_SHA1
+ #integration_testing:
+ # executor: go
+ # steps:
+ # - attach_workspace:
+ # at: .
+ # - run: go build .
+ # - setup_remote_docker
+ # - run: ./scripts/lifecycle-tests.sh
build:
executor: go
steps:
@@ -203,3 +233,24 @@ jobs:
-e DOCKER_REPOSITORY=containrrr/watchtower \
-e GIT_BRANCH=master \
lsiodev/readme-sync bash -c 'node sync'
+ publish-docs:
+ executor: py
+ steps:
+ - attach_workspace:
+ at: .
+ - run:
+ name: Install prerequisites
+ command: |
+ sudo pip install \
+ mkdocs \
+ mkdocs-material \
+ md-toc
+ - add_ssh_keys:
+ fingerprints:
+ - "91:75:47:15:b2:8e:85:e5:67:0e:63:7f:22:d2:b4:6e"
+ - run:
+ name: Generate and publish
+ command: |
+ mkdir ~/.ssh && touch ~/.ssh/known_hosts;
+ ssh-keyscan -H github.com >> ~/.ssh/known_hosts && \
+ mkdocs gh-deploy
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..364d198
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+custom: https://www.amazon.com/hz/wishlist/ls/F94JJV822VX6
diff --git a/.github/config.yml b/.github/config.yml
new file mode 100644
index 0000000..62993b5
--- /dev/null
+++ b/.github/config.yml
@@ -0,0 +1,11 @@
+newIssueWelcomeComment: >
+ Hi there!
+
+ Thanks a bunch for opening your first issue! :pray:
+ As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/containrrr/watchtower/blob/master/CODE_OF_CONDUCT.md)
+
+newPRWelcomeComment: >
+ Thanks for opening this pull request! Please check out our [contributing guidelines](https://github.com/containrrr/watchtower/blob/master/CONTRIBUTING.md) as well as our [code of conduct](https://github.com/containrrr/watchtower/blob/master/CODE_OF_CONDUCT.md).
+
+firstPRMergeComment: >
+ Congrats on merging your first pull request! We are all very proud of you! :sparkles:
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000..1b245b9
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,14 @@
+daysUntilStale: 60
+daysUntilClose: 7
+exemptMilestones: true
+exemptLabels:
+ - "Public Service Announcement"
+ - "Do not close"
+ - "Type: Bug"
+ - "Type: Security"
+staleLabel: "Status: Stale"
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+closeComment: false
diff --git a/.gitignore b/.gitignore
index 8195f6f..fda8d42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ vendor
.glide
dist
.idea
-.DS_Store
\ No newline at end of file
+.DS_Store
+/site
diff --git a/README.md b/README.md
index 2827bfe..b985496 100644
--- a/README.md
+++ b/README.md
@@ -35,310 +35,88 @@
+
+
+
+
+
+
-## Overview
+## Quick Start
-Watchtower is an application that will monitor your running Docker containers and watch for changes to the images that those containers were originally started from. If watchtower detects that an image has changed, it will automatically restart the container using the new image.
+With watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially. Run the watchtower container with the following command:
-With watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially.
-
-For example, let's say you were running watchtower along with an instance of _centurylink/wetty-cli_ image:
-
-```bash
-$ docker ps
-CONTAINER ID IMAGE STATUS PORTS NAMES
-967848166a45 centurylink/wetty-cli Up 10 minutes 0.0.0.0:8080->3000/tcp wetty
-6cc4d2a9d1a5 containrrr/watchtower Up 15 minutes watchtower
+```
+$ docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ containrrr/watchtower
```
-Every few minutes watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping).
-
-## Usage
-
-Watchtower is itself packaged as a Docker container so installation is as simple as pulling the `containrrr/watchtower` image. If you are using ARM based architecture, pull the appropriate `containrrr/watchtower:armhf-` image from the [containrrr Docker Hub](https://hub.docker.com/r/containrrr/watchtower/tags/).
-
-Since the watchtower code needs to interact with the Docker API in order to monitor the running containers, you need to mount _/var/run/docker.sock_ into the container with the -v flag when you run it.
-
-Run the `watchtower` container with the following command:
-
-```bash
-docker run -d \
- --name watchtower \
- -v /var/run/docker.sock:/var/run/docker.sock \
- containrrr/watchtower
-```
-
-If pulling images from private Docker registries, supply registry authentication credentials with the environment variables `REPO_USER` and `REPO_PASS`
-or by mounting the host's docker config file into the container (at the root of the container filesystem `/`).
-
-Passing environment variables:
-
-```bash
-docker run -d \
- --name watchtower \
- -e REPO_USER=username \
- -e REPO_PASS=password \
- -v /var/run/docker.sock:/var/run/docker.sock \
- containrrr/watchtower container_to_watch --debug
-```
-
-Also check out [this Stack Overflow answer](https://stackoverflow.com/a/30494145/7872793) for more options on how to pass environment variables.
-
-Mounting the host's docker config file:
-
-```bash
-docker run -d \
- --name watchtower \
- -v /home//.docker/config.json:/config.json \
- -v /var/run/docker.sock:/var/run/docker.sock \
- containrrr/watchtower container_to_watch --debug
-```
-
-If you mount the config file as described above, be sure to also prepend the url for the registry when starting up your watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container from a private repo at dockerhub and monitors it with watchtower. Note the command argument changing the interval to 30s rather than the default 5 minutes.
-
-```json
-version: "3"
-services:
- cavo:
- image: index.docker.io//:
- ports:
- - "443:3443"
- - "80:3080"
- watchtower:
- image: containrrr/watchtower
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock
- - /root/.docker/config.json:/config.json
- command: --interval 30
-```
-
-### Arguments
-
-By default, watchtower will monitor all containers running within the Docker daemon to which it is pointed (in most cases this will be the local Docker daemon, but you can override it with the `--host` option described in the next section). However, you can restrict watchtower to monitoring a subset of the running containers by specifying the container names as arguments when launching watchtower.
-
-```bash
-docker run -d \
- --name watchtower \
- -v /var/run/docker.sock:/var/run/docker.sock \
- containrrr/watchtower nginx redis
-```
-
-In the example above, watchtower will only monitor the containers named "nginx" and "redis" for updates -- all of the other running containers will be ignored.
-
-If you do not want watchtower to run as a daemon you can pass a run-once flag and remove the watchtower container after it's execution.
-
-```bash
-docker run --rm \
--v /var/run/docker.sock:/var/run/docker.sock \
-containrrr/watchtower --run-once nginx redis
-```
-
-In the example above, watchtower will execute an upgrade attempt on the containers named "nginx" and "redis". Using this mode will enable debugging output showing all actions performed as usage is intended for interactive users. Once the attempt is completed, the container will exit and remove itself due to the "--rm" flag.
-
-When no arguments are specified, watchtower will monitor all running containers.
-
-### Options
-
-Any of the options described below can be passed to the watchtower process by setting them after the image name in the `docker run` string:
-
-```bash
-docker run --rm containrrr/watchtower --help
-```
-
-- `--host, -h` Docker daemon socket to connect to. Defaults to "unix:///var/run/docker.sock" but can be pointed at a remote Docker host by specifying a TCP endpoint as "tcp://hostname:port". The host value can also be provided by setting the `DOCKER_HOST` environment variable.
-- `--run-once` Run an update attempt against a container name list one time immediately and exit.
-- `--interval, -i` Poll interval (in seconds). This value controls how frequently watchtower will poll for new images. Defaults to 300 seconds (5 minutes).
-- `--schedule, -s` [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 * * *"`
-- `--no-pull` Do not pull new images. When this flag is specified, watchtower will not attempt to pull new images from the registry. Instead it will only monitor the local image cache for changes. Use this option if you are building new images directly on the Docker host without pushing them to a registry.
-- `--stop-timeout` Timeout before the container is forcefully stopped. When set, this option will change the default (`10s`) wait time to the given value. An example: `--stop-timeout 30s` will set the timeout to 30 seconds.
-- `--label-enable` Watch containers where the `com.centurylinklabs.watchtower.enable` label is set to true.
-- `--cleanup` Remove old images after updating. When this flag is specified, watchtower will remove the old image after restarting a container with a new image. Use this option to prevent the accumulation of orphaned images on your system as containers are updated.
-- `--tlsverify` Use TLS when connecting to the Docker socket and verify the server's certificate.
-- `--debug` Enable debug mode. When this option is specified you'll see more verbose logging in the watchtower log file.
-- `--monitor-only` Will only monitor for new images, not update the containers.
-- `--help` Show documentation about the supported flags.
-
-See below for options used to configure notifications.
-
-## Linked Containers
-
-Watchtower will detect if there are links between any of the running containers and ensure that things are stopped/started in a way that won't break any of the links. If an update is detected for one of the dependencies in a group of linked containers, watchtower will stop and start all of the containers in the correct order so that the application comes back up correctly.
-
-For example, imagine you were running a _mysql_ container and a _wordpress_ container which had been linked to the _mysql_ container. If watchtower were to detect that the _mysql_ container required an update, it would first shut down the linked _wordpress_ container followed by the _mysql_ container. When restarting the containers it would handle _mysql_ first and then _wordpress_ to ensure that the link continued to work.
-
-## Stopping Containers
-
-When watchtower detects that a running container needs to be updated it will stop the container by sending it a SIGTERM signal.
-If your container should be shutdown with a different signal you can communicate this to watchtower by setting a label named _com.centurylinklabs.watchtower.stop-signal_ with the value of the desired signal.
-
-This label can be coded directly into your image by using the `LABEL` instruction in your Dockerfile:
-
-```docker
-LABEL com.centurylinklabs.watchtower.stop-signal="SIGHUP"
-```
-
-Or, it can be specified as part of the `docker run` command line:
-
-```bash
-docker run -d --label=com.centurylinklabs.watchtower.stop-signal=SIGHUP someimage
-```
-
-## Selectively Watching Containers
-
-By default, watchtower will watch all containers. However, sometimes only some containers should be updated.
-
-If you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`.
-
-```docker
-LABEL com.centurylinklabs.watchtower.enable="false"
-```
-
-Or, it can be specified as part of the `docker run` command line:
-
-```bash
-docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage
-```
-
-If you need to only 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.
-
-```docker
-LABEL com.centurylinklabs.watchtower.enable="true"
-```
-
-Or, it can be specified as part of the `docker run` command line:
-
-```bash
-docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage
-```
-
-## Remote Hosts
-
-By default, watchtower is set-up to monitor the local Docker daemon (the same daemon running the watchtower container itself). However, it is possible to configure watchtower to monitor a remote Docker endpoint. When starting the watchtower container you can specify a remote Docker endpoint with either the `--host` flag or the `DOCKER_HOST` environment variable:
-
-```bash
-docker run -d \
- --name watchtower \
- containrrr/watchtower --host "tcp://10.0.1.2:2375"
-```
-
-or
-
-```bash
-docker run -d \
- --name watchtower \
- -e DOCKER_HOST="tcp://10.0.1.2:2375" \
- containrrr/watchtower
-```
-
-Note in both of the examples above that it is unnecessary to mount the _/var/run/docker.sock_ into the watchtower container.
-
-### Secure Connections
-
-Watchtower is also capable of connecting to Docker endpoints which are protected by SSL/TLS. If you've used _docker-machine_ to provision your remote Docker host, you simply need to volume mount the certificates generated by _docker-machine_ into the watchtower container and optionally specify `--tlsverify` flag.
-
-The _docker-machine_ certificates for a particular host can be located by executing the `docker-machine env` command for the desired host (note the values for the `DOCKER_HOST` and `DOCKER_CERT_PATH` environment variables that are returned from this command). The directory containing the certificates for the remote host needs to be mounted into the watchtower container at _/etc/ssl/docker_.
-
-With the certificates mounted into the watchtower container you need to specify the `--tlsverify` flag to enable verification of the certificate:
-
-```bash
-docker run -d \
- --name watchtower \
- -e DOCKER_HOST=$DOCKER_HOST \
- -e DOCKER_CERT_PATH=/etc/ssl/docker \
- -v $DOCKER_CERT_PATH:/etc/ssl/docker \
- containrrr/watchtower --tlsverify
-```
-
-## Updating Watchtower
-
-If watchtower is monitoring the same Docker daemon under which the watchtower container itself is running (i.e. if you volume-mounted _/var/run/docker.sock_ into the watchtower container) then it has the ability to update itself. If a new version of the _containrrr/watchtower_ image is pushed to the Docker Hub, your watchtower will pull down the new image and restart itself automatically.
-
-## Notifications
-
-Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging system, [logrus](http://github.com/sirupsen/logrus).
-The types of notifications to send are passed via the comma-separated option `--notifications` (or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
-
-- `email` to send notifications via e-mail
-- `slack` to send notifications through a Slack webhook
-- `msteams` to send notifications via MSTeams webhook
-
-### Settings
-
-- `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info` or `debug`.
-
-### Notifications via E-Mail
-
-To receive notifications by email, the following command-line options, or their corresponding environment variables, can be set:
-
-- `--notification-email-from` (env. `WATCHTOWER_NOTIFICATION_EMAIL_FROM`): The e-mail address from which notifications will be sent.
-- `--notification-email-to` (env. `WATCHTOWER_NOTIFICATION_EMAIL_TO`): The e-mail address to which notifications will be sent.
-- `--notification-email-server` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER`): The SMTP server to send e-mails through.
-- `--notification-email-server-tls-skip-verify` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY`): Do not verify the TLS certificate of the mail server. This should be used only for testing.
-- `--notification-email-server-port` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT`): The port used to connect to the SMTP server to send e-mails through. Defaults to `25`.
-- `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.
-- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with.
-
-Example:
-
-```bash
-docker run -d \
- --name watchtower \
- -v /var/run/docker.sock:/var/run/docker.sock \
- -e WATCHTOWER_NOTIFICATIONS=email \
- -e WATCHTOWER_NOTIFICATION_EMAIL_FROM=fromaddress@gmail.com \
- -e WATCHTOWER_NOTIFICATION_EMAIL_TO=toaddress@gmail.com \
- -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com \
- -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=fromaddress@gmail.com \
- -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \
- containrrr/watchtower
-```
-
-### Notifications through Slack webhook
-
-To receive notifications in Slack, add `slack` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
-
-Additionally, you should set the Slack webhook url using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable.
-
-By default, watchtower will send messages under the name `watchtower`, you can customize this string through the `--notification-slack-identifier` option or the `WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER` environment variable.
-
-Other, optional, variables include:
-
-- `--notification-slack-channel` (env. `WATCHTOWER_NOTIFICATION_SLACK_CHANNEL`): A string which overrides the webhook's default channel. Example: #my-custom-channel.
-- `--notification-slack-icon-emoji` (env. `WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI`): An [emoji code](https://www.webpagefx.com/tools/emoji-cheat-sheet/) string to use in place of the default icon.
-- `--notification-slack-icon-url` (env. `WATCHTOWER_NOTIFICATION_SLACK_ICON_URL`): An icon image URL string to use in place of the default icon.
-
-Example:
-
-```bash
-docker run -d \
- --name watchtower \
- -v /var/run/docker.sock:/var/run/docker.sock \
- -e WATCHTOWER_NOTIFICATIONS=slack \
- -e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL="https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy" \
- -e WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=watchtower-server-1 \
- -e WATCHTOWER_NOTIFICATION_SLACK_CHANNEL=#my-custom-channel \
- -e WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI=:whale: \
- -e WATCHTOWER_NOTIFICATION_SLACK_ICON_URL= \
- containrrr/watchtower
-```
-
-### Notifications via MSTeams incoming webhook
-
-To receive notifications in MSTeams channel, add `msteams` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
-
-Additionally, you should set the MSTeams webhook url using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable.
-
-MSTeams notifier could send keys/values filled by `log.WithField` or `log.WithFields` as MSTeams message facts. To enable this feature add `--notification-msteams-data` flag or set `WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true` environment variable.
-
-Example:
-
-```bash
-docker run -d \
- --name watchtower \
- -v /var/run/docker.sock:/var/run/docker.sock \
- -e WATCHTOWER_NOTIFICATIONS=msteams \
- -e WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL="https://outlook.office.com/webhook/xxxxxxxx@xxxxxxx/IncomingWebhook/yyyyyyyy/zzzzzzzzzz" \
- -e WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true \
- containrrr/watchtower
-```
+## Documentation
+The full documentation is available at https://containrrr.github.io/watchtower.
+
+## Contributors
+
+Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
diff --git a/_config.yml b/_config.yml
deleted file mode 100644
index 2f7efbe..0000000
--- a/_config.yml
+++ /dev/null
@@ -1 +0,0 @@
-theme: jekyll-theme-minimal
\ No newline at end of file
diff --git a/actions/update.go b/actions/update.go
deleted file mode 100644
index a18ea4c..0000000
--- a/actions/update.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package actions
-
-import (
- "math/rand"
- "time"
-
- "github.com/containrrr/watchtower/container"
- log "github.com/sirupsen/logrus"
-)
-
-var (
- letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
-)
-
-// UpdateParams contains all different options available to alter the behavior of the Update func
-type UpdateParams struct {
- Filter container.Filter
- Cleanup bool
- NoRestart bool
- Timeout time.Duration
- MonitorOnly bool
-}
-
-// Update looks at the running Docker containers to see if any of the images
-// used to start those containers have been updated. If a change is detected in
-// any of the images, the associated containers are stopped and restarted with
-// the new image.
-func Update(client container.Client, params UpdateParams) error {
- log.Debug("Checking containers for updated images")
-
- containers, err := client.ListContainers(params.Filter)
- if err != nil {
- return err
- }
-
- for i, container := range containers {
- stale, err := client.IsContainerStale(container)
- if err != nil {
- log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
- log.Debug(err)
- stale = false
- }
- containers[i].Stale = stale
- }
-
- containers, err = container.SortByDependencies(containers)
- if err != nil {
- return err
- }
-
- checkDependencies(containers)
-
- if params.MonitorOnly {
- return nil
- }
-
- // Stop stale containers in reverse order
- for i := len(containers) - 1; i >= 0; i-- {
- container := containers[i]
-
- if container.IsWatchtower() {
- log.Debugf("This is the watchtower container %s", containers[i].Name())
- continue
- }
-
- if container.Stale {
- if err := client.StopContainer(container, params.Timeout); err != nil {
- log.Error(err)
- }
- }
- }
-
- // Restart stale containers in sorted order
- for _, container := range containers {
- if container.Stale {
- // Since we can't shutdown a watchtower container immediately, we need to
- // start the new one while the old one is still running. This prevents us
- // from re-using the same container name so we first rename the current
- // instance so that the new one can adopt the old name.
- if container.IsWatchtower() {
- if err := client.RenameContainer(container, randName()); err != nil {
- log.Error(err)
- continue
- }
- }
-
- if !params.NoRestart {
- if err := client.StartContainer(container); err != nil {
- log.Error(err)
- }
- }
-
- if params.Cleanup {
- client.RemoveImage(container)
- }
- }
- }
-
- return nil
-}
-
-func checkDependencies(containers []container.Container) {
-
- for i, parent := range containers {
- if parent.Stale {
- continue
- }
-
- LinkLoop:
- for _, linkName := range parent.Links() {
- for _, child := range containers {
- if child.Name() == linkName && child.Stale {
- containers[i].Stale = true
- break LinkLoop
- }
- }
- }
- }
-}
-
-// Generates a random, 32-character, Docker-compatible container name.
-func randName() string {
- b := make([]rune, 32)
- for i := range b {
- b[i] = letters[rand.Intn(len(letters))]
- }
-
- return string(b)
-}
diff --git a/app/app.go b/app/app.go
deleted file mode 100644
index 5218d5d..0000000
--- a/app/app.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package app
-
-import (
- "time"
-
- "github.com/urfave/cli"
-)
-
-// SetupCliFlags registers flags on the supplied urfave app
-func SetupCliFlags(app *cli.App) {
- app.Flags = []cli.Flag{
- cli.StringFlag{
- Name: "host, H",
- Usage: "daemon socket to connect to",
- Value: "unix:///var/run/docker.sock",
- EnvVar: "DOCKER_HOST",
- },
- cli.IntFlag{
- Name: "interval, i",
- Usage: "poll interval (in seconds)",
- Value: 300,
- EnvVar: "WATCHTOWER_POLL_INTERVAL",
- },
- cli.StringFlag{
- Name: "schedule, s",
- Usage: "the cron expression which defines when to update",
- EnvVar: "WATCHTOWER_SCHEDULE",
- },
- cli.BoolFlag{
- Name: "no-pull",
- Usage: "do not pull new images",
- EnvVar: "WATCHTOWER_NO_PULL",
- },
- cli.BoolFlag{
- Name: "no-restart",
- Usage: "do not restart containers",
- EnvVar: "WATCHTOWER_NO_RESTART",
- },
- cli.BoolFlag{
- Name: "cleanup",
- Usage: "remove old images after updating",
- EnvVar: "WATCHTOWER_CLEANUP",
- },
- cli.BoolFlag{
- Name: "tlsverify",
- Usage: "use TLS and verify the remote",
- EnvVar: "DOCKER_TLS_VERIFY",
- },
- cli.DurationFlag{
- Name: "stop-timeout",
- Usage: "timeout before container is forcefully stopped",
- Value: time.Second * 10,
- EnvVar: "WATCHTOWER_TIMEOUT",
- },
- cli.BoolFlag{
- Name: "label-enable",
- Usage: "watch containers where the com.centurylinklabs.watchtower.enable label is true",
- EnvVar: "WATCHTOWER_LABEL_ENABLE",
- },
- cli.BoolFlag{
- Name: "debug",
- Usage: "enable debug mode with verbose logging",
- },
- cli.StringSliceFlag{
- Name: "notifications",
- Value: &cli.StringSlice{},
- Usage: "notification types to send (valid: email, slack, msteams)",
- EnvVar: "WATCHTOWER_NOTIFICATIONS",
- },
- cli.StringFlag{
- Name: "notifications-level",
- Usage: "The log level used for sending notifications. Possible values: \"panic\", \"fatal\", \"error\", \"warn\", \"info\" or \"debug\"",
- EnvVar: "WATCHTOWER_NOTIFICATIONS_LEVEL",
- Value: "info",
- },
- cli.StringFlag{
- Name: "notification-email-from",
- Usage: "Address to send notification e-mails from",
- EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_FROM",
- },
- cli.StringFlag{
- Name: "notification-email-to",
- Usage: "Address to send notification e-mails to",
- EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_TO",
- },
- cli.StringFlag{
- Name: "notification-email-server",
- Usage: "SMTP server to send notification e-mails through",
- EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER",
- },
- cli.IntFlag{
- Name: "notification-email-server-port",
- Usage: "SMTP server port to send notification e-mails through",
- Value: 25,
- EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT",
- },
- cli.BoolFlag{
- Name: "notification-email-server-tls-skip-verify",
- Usage: "Controls whether watchtower verifies the SMTP server's certificate chain and host name. " +
- "If set, TLS accepts any certificate " +
- "presented by the server and any host name in that certificate. " +
- "In this mode, TLS is susceptible to man-in-the-middle attacks. " +
- "This should be used only for testing.",
- EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY",
- },
- cli.StringFlag{
- Name: "notification-email-server-user",
- Usage: "SMTP server user for sending notifications",
- EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER",
- },
- cli.StringFlag{
- Name: "notification-email-server-password",
- Usage: "SMTP server password for sending notifications",
- EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD",
- },
- cli.StringFlag{
- Name: "notification-slack-hook-url",
- Usage: "The Slack Hook URL to send notifications to",
- EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL",
- },
- cli.StringFlag{
- Name: "notification-slack-identifier",
- Usage: "A string which will be used to identify the messages coming from this watchtower instance. Default if omitted is \"watchtower\"",
- EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER",
- Value: "watchtower",
- },
- cli.StringFlag{
- Name: "notification-slack-channel",
- Usage: "A string which overrides the webhook's default channel. Example: #my-custom-channel",
- EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_CHANNEL",
- },
- cli.StringFlag{
- Name: "notification-slack-icon-emoji",
- Usage: "An emoji code string to use in place of the default icon",
- EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI",
- },
- cli.StringFlag{
- Name: "notification-slack-icon-url",
- Usage: "An icon image URL string to use in place of the default icon",
- EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_ICON_URL",
- },
- cli.StringFlag{
- Name: "notification-msteams-hook",
- Usage: "The MSTeams WebHook URL to send notifications to",
- EnvVar: "WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL",
- },
- cli.BoolFlag{
- Name: "notification-msteams-data",
- Usage: "The MSTeams notifier will try to extract log entry fields as MSTeams message facts",
- EnvVar: "WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA",
- },
- cli.BoolFlag{
- Name: "monitor-only",
- Usage: "Will only monitor for new images, not update the containers",
- EnvVar: "WATCHTOWER_MONITOR_ONLY",
- },
- cli.BoolFlag{
- Name: "run-once",
- Usage: "Run once now and exit",
- },
- }
-}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..ea00786
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,186 @@
+package cmd
+
+import (
+ "os"
+ "os/signal"
+ "strconv"
+ "syscall"
+ "time"
+
+ "github.com/containrrr/watchtower/internal/actions"
+ "github.com/containrrr/watchtower/internal/flags"
+ "github.com/containrrr/watchtower/pkg/container"
+ "github.com/containrrr/watchtower/pkg/notifications"
+ t "github.com/containrrr/watchtower/pkg/types"
+ "github.com/robfig/cron"
+ log "github.com/sirupsen/logrus"
+
+ "github.com/spf13/cobra"
+)
+
+var (
+ client container.Client
+ scheduleSpec string
+ cleanup bool
+ noRestart bool
+ monitorOnly bool
+ enableLabel bool
+ notifier *notifications.Notifier
+ timeout time.Duration
+ lifecycleHooks bool
+)
+
+var rootCmd = &cobra.Command{
+ Use: "watchtower",
+ Short: "Automatically updates running Docker containers",
+ Long: `
+Watchtower automatically updates running Docker containers whenever a new image is released.
+More information available at https://github.com/containrrr/watchtower/.
+`,
+ Run: Run,
+ PreRun: PreRun,
+}
+
+func init() {
+ flags.SetDefaults()
+ flags.RegisterDockerFlags(rootCmd)
+ flags.RegisterSystemFlags(rootCmd)
+ flags.RegisterNotificationFlags(rootCmd)
+}
+
+// Execute the root func and exit in case of errors
+func Execute() {
+ if err := rootCmd.Execute(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+// PreRun is a lifecycle hook that runs before the command is executed.
+func PreRun(cmd *cobra.Command, args []string) {
+ f := cmd.PersistentFlags()
+
+ if enabled, _ := f.GetBool("debug"); enabled {
+ log.SetLevel(log.DebugLevel)
+ }
+
+ pollingSet := f.Changed("interval")
+ schedule, _ := f.GetString("schedule")
+ cronLen := len(schedule)
+
+ if pollingSet && cronLen > 0 {
+ log.Fatal("Only schedule or interval can be defined, not both.")
+ } else if cronLen > 0 {
+ scheduleSpec, _ = f.GetString("schedule")
+ } else {
+ interval, _ := f.GetInt("interval")
+ scheduleSpec = "@every " + strconv.Itoa(interval) + "s"
+ }
+
+ cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
+
+ if timeout < 0 {
+ log.Fatal("Please specify a positive value for timeout value.")
+ }
+
+ enableLabel, _ = f.GetBool("label-enable")
+ lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
+
+ // configure environment vars for client
+ err := flags.EnvConfig(cmd)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ noPull, _ := f.GetBool("no-pull")
+ includeStopped, _ := f.GetBool("include-stopped")
+ removeVolumes, _ := f.GetBool("remove-volumes")
+
+ client = container.NewClient(
+ !noPull,
+ includeStopped,
+ removeVolumes,
+ )
+
+ notifier = notifications.NewNotifier(cmd)
+}
+
+// Run is the main execution flow of the command
+func Run(c *cobra.Command, names []string) {
+ filter := container.BuildFilter(names, enableLabel)
+ runOnce, _ := c.PersistentFlags().GetBool("run-once")
+
+ if runOnce {
+ log.Info("Running a one time update.")
+ runUpdatesWithNotifications(filter)
+ os.Exit(0)
+ return
+ }
+
+ if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup); err != nil {
+ log.Fatal(err)
+ }
+
+ if err := runUpgradesOnSchedule(filter); err != nil {
+ log.Error(err)
+ }
+
+ os.Exit(1)
+}
+
+func runUpgradesOnSchedule(filter t.Filter) error {
+ tryLockSem := make(chan bool, 1)
+ tryLockSem <- true
+
+ cron := cron.New()
+ err := cron.AddFunc(
+ scheduleSpec,
+ func() {
+ select {
+ case v := <-tryLockSem:
+ defer func() { tryLockSem <- v }()
+ runUpdatesWithNotifications(filter)
+ default:
+ log.Debug("Skipped another update already running.")
+ }
+
+ nextRuns := cron.Entries()
+ if len(nextRuns) > 0 {
+ log.Debug("Scheduled next run: " + nextRuns[0].Next.String())
+ }
+ })
+
+ if err != nil {
+ return err
+ }
+
+ log.Debug("Starting Watchtower and scheduling first run: " + cron.Entries()[0].Schedule.Next(time.Now()).String())
+ cron.Start()
+
+ // Graceful shut-down on SIGINT/SIGTERM
+ interrupt := make(chan os.Signal, 1)
+ signal.Notify(interrupt, os.Interrupt)
+ signal.Notify(interrupt, syscall.SIGTERM)
+
+ <-interrupt
+ cron.Stop()
+ log.Info("Waiting for running update to be finished...")
+ <-tryLockSem
+ return nil
+}
+
+func runUpdatesWithNotifications(filter t.Filter) {
+ notifier.StartNotification()
+ updateParams := actions.UpdateParams{
+ Filter: filter,
+ Cleanup: cleanup,
+ NoRestart: noRestart,
+ Timeout: timeout,
+ MonitorOnly: monitorOnly,
+ LifecycleHooks: lifecycleHooks,
+ }
+ err := actions.Update(client, updateParams)
+ if err != nil {
+ log.Println(err)
+ }
+ notifier.SendNotification()
+}
diff --git a/docs/arguments.md b/docs/arguments.md
new file mode 100644
index 0000000..db51c95
--- /dev/null
+++ b/docs/arguments.md
@@ -0,0 +1,181 @@
+By default, watchtower will monitor all containers running within the Docker daemon to which it is pointed (in most cases this
+will be the local Docker daemon, but you can override it with the `--host` option described in the next section). However, you
+can restrict watchtower to monitoring a subset of the running containers by specifying the container names as arguments when
+launching watchtower.
+
+```bash
+$ docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ containrrr/watchtower \
+ nginx redis
+```
+
+In the example above, watchtower will only monitor the containers named "nginx" and "redis" for updates -- all of the other
+running containers will be ignored. If you do not want watchtower to run as a daemon you can pass the `--run-once` flag and remove
+the watchtower container after its execution.
+
+```bash
+$ docker run --rm \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ containrrr/watchtower \
+ --run-once \
+ nginx redis
+```
+
+In the example above, watchtower will execute an upgrade attempt on the containers named "nginx" and "redis". Using this mode will enable debugging output showing all actions performed, as usage is intended for interactive users. Once the attempt is completed, the container will exit and remove itself due to the `--rm` flag.
+
+When no arguments are specified, watchtower will monitor all running containers.
+
+## Help
+Shows documentation about the supported flags.
+
+```
+ Argument: --help
+Environment Variable: N/A
+ Type: N/A
+ Default: N/A
+```
+
+## Cleanup
+Removes old images after updating. When this flag is specified, watchtower will remove the old image after restarting a container with a new image. Use this option to prevent the accumulation of orphaned images on your system as containers are updated.
+
+```
+ Argument: --cleanup
+Environment Variable: WATCHTOWER_CLEANUP
+ Type: Boolean
+ Default: false
+```
+
+## Remove attached volumes
+Removes attached volumes after updating. When this flag is specified, watchtower will remove all attached volumes from the container before restarting with a new image. Use this option to force new volumes to be populated as containers are updated.
+
+```
+ Argument: --remove-volumes
+Environment Variable: WATCHTOWER_REMOVE_VOLUMES
+ Type: Boolean
+ Default: false
+```
+
+## Debug
+Enable debug mode with verbose logging.
+
+```
+ Argument: --debug
+Environment Variable: N/A
+ Type: Boolean
+ Default: false
+```
+
+## Docker host
+Docker daemon socket to connect to. Can be pointed at a remote Docker host by specifying a TCP endpoint as "tcp://hostname:port".
+
+```
+ Argument: --host, -h
+Environment Variable: DOCKER_HOST
+ Type: String
+ Default: "unix:///var/run/docker.sock"
+```
+
+## Docker API version
+The API version to use by the Docker client for connecting to the Docker daemon. The minimum supported version is 1.24.
+
+```
+ Argument: --api-version, -a
+Environment Variable: DOCKER_API_VERSION
+ Type: String
+ Default: "1.24"
+```
+
+## Include stopped
+Will also include created and exited containers.
+
+```
+ Argument: --include-stopped
+Environment Variable: WATCHTOWER_INCLUDE_STOPPED
+ Type: Boolean
+ Default: false
+```
+
+## Poll interval
+Poll interval (in seconds). This value controls how frequently watchtower will poll for new images.
+
+```
+ Argument: --interval, -i
+Environment Variable: WATCHTOWER_POLL_INTERVAL
+ Type: Integer
+ Default: 300
+```
+
+## Filter by enable label
+Update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
+
+```
+ Argument: --label-enable
+Environment Variable: WATCHTOWER_LABEL_ENABLE
+ Type: Boolean
+ Default: false
+```
+
+## Without updating containers
+Will only monitor for new images, not update the containers.
+
+```
+ Argument: --monitor-only
+Environment Variable: WATCHTOWER_MONITOR_ONLY
+ Type: Boolean
+ Default: false
+```
+
+## Without pulling new images
+Do not pull new images. When this flag is specified, watchtower will not attempt to pull
+new images from the registry. Instead it will only monitor the local image cache for changes.
+Use this option if you are building new images directly on the Docker host without pushing
+them to a registry.
+
+```
+ Argument: --no-pull
+Environment Variable: WATCHTOWER_NO_PULL
+ Type: Boolean
+ Default: false
+```
+
+## Run once
+Run an update attempt against a container name list one time immediately and exit.
+
+```
+ Argument: --run-once
+Environment Variable: WATCHTOWER_RUN_ONCE
+ Type: Boolean
+ Default: false
+```
+
+## 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 * * *"`
+
+```
+ Argument: --schedule, -s
+Environment Variable: WATCHTOWER_SCHEDULE
+ Type: String
+ Default: -
+```
+
+## Wait until timeout
+Timeout before the container is forcefully stopped. When set, this option will change the default (`10s`) wait time to the given value. An example: `--stop-timeout 30s` will set the timeout to 30 seconds.
+
+```
+ Argument: --stop-timeout
+Environment Variable: WATCHTOWER_TIMEOUT
+ Type: Duration
+ Default: 10s
+```
+
+## TLS Verification
+Use TLS when connecting to the Docker socket and verify the server's certificate. See below for options used to configure notifications.
+
+```
+ Argument: --tlsverify
+Environment Variable: DOCKER_TLS_VERIFY
+ Type: Boolean
+ Default: false
+```
diff --git a/docs/container-selection.md b/docs/container-selection.md
new file mode 100644
index 0000000..4c3312c
--- /dev/null
+++ b/docs/container-selection.md
@@ -0,0 +1,25 @@
+By default, watchtower will watch all containers. However, sometimes only some containers should be updated.
+
+If you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`.
+
+```docker
+LABEL com.centurylinklabs.watchtower.enable="false"
+```
+
+Or, it can be specified as part of the `docker run` command line:
+
+```bash
+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.
+
+```docker
+LABEL com.centurylinklabs.watchtower.enable="true"
+```
+
+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/credential-helpers.md b/docs/credential-helpers.md
new file mode 100644
index 0000000..1722906
--- /dev/null
+++ b/docs/credential-helpers.md
@@ -0,0 +1,64 @@
+Some private docker registries (the most prominent probably being AWS ECR) use non-standard ways of authentication.
+To be able to use this together with watchtower, we need to use a credential helper.
+
+To keep the image size small we've decided to not include any helpers in the watchtower image, instead we'll put the
+helper in a separate container and mount it using volumes.
+
+### Example
+Example implementation for use with [amazon-ecr-credential-helper](https://github.com/awslabs/amazon-ecr-credential-helper):
+
+```Dockerfile
+FROM golang:latest
+
+ENV CGO_ENABLED 0
+ENV REPO github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login
+
+RUN go get -u $REPO
+
+RUN rm /go/bin/docker-credential-ecr-login
+
+RUN go build \
+ -o /go/bin/docker-credential-ecr-login \
+ /go/src/$REPO
+
+WORKDIR /go/bin/
+```
+
+and the docker-compose definition:
+```yaml
+version: "3"
+
+services:
+ watchtower:
+ image: index.docker.io/containrrr/watchtower:latest
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - /.docker/config.json:/config.json
+ - helper:/go/bin
+ environment:
+ - HOME=/
+ - PATH=$PATH:/go/bin
+ - AWS_REGION=
+ - AWS_ACCESS_KEY_ID=
+ - AWS_SECRET_ACCESS_KEY=
+volumes:
+ helper: {}
+```
+
+and for `.docker/config.yml`:
+```yaml
+ {
+ "HttpHeaders" : {
+ "User-Agent" : "Docker-Client/19.03.1 (XXXXXX)"
+ },
+ "credsStore" : "osxkeychain", // ...or your prefered helper
+ "auths" : {
+ "xyzxyzxyz.dkr.ecr.eu-north-1.amazonaws.com" : {},
+ "https://index.docker.io/v1/": {}
+ },
+ "credHelpers": {
+ "xyzxyzxyz.dkr.ecr.eu-north-1.amazonaws.com" : "ecr-login",
+ "index.docker.io": "osxkeychain" // ...or your prefered helper
+ }
+ }
+```
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..c0bab4c
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,49 @@
+
+ Watchtower
+
+
+
+ A container-based solution for automating Docker container base image updates.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Quick Start
+
+With watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially. Run the watchtower container with the following command:
+
+```
+$ docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ containrrr/watchtower
+```
\ No newline at end of file
diff --git a/docs/introduction.md b/docs/introduction.md
new file mode 100644
index 0000000..9e0f5fe
--- /dev/null
+++ b/docs/introduction.md
@@ -0,0 +1,15 @@
+Watchtower is an application that will monitor your running Docker containers and watch for changes to the images that those containers were originally started from. If watchtower detects that an image has changed, it will automatically restart the container using the new image.
+
+With watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially.
+
+For example, let's say you were running watchtower along with an instance of _centurylink/wetty-cli_ image:
+
+```bash
+$ docker ps
+CONTAINER ID IMAGE STATUS PORTS NAMES
+967848166a45 centurylink/wetty-cli Up 10 minutes 0.0.0.0:8080->3000/tcp wetty
+6cc4d2a9d1a5 containrrr/watchtower Up 15 minutes watchtower
+```
+
+Every few minutes watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping).
+
diff --git a/docs/lifecycle-hooks.md b/docs/lifecycle-hooks.md
new file mode 100644
index 0000000..97d254e
--- /dev/null
+++ b/docs/lifecycle-hooks.md
@@ -0,0 +1,45 @@
+
+## Executing commands before and after updating
+
+> **DO NOTE**: Both commands are shell commands executed with `sh`, and therefore require the
+> container to provide the `sh` executable.
+
+It is possible to execute a *pre-update* command and a *post-update* command
+**inside** every container updated by watchtower. The *pre-update* command is
+executed before stopping the container, and the *post-update* command is
+executed after restarting the container.
+
+This feature is disabled by default. To enable it, you need to set the option
+`--enable-lifecycle-hooks` on the command line, or set the environment variable
+`WATCHTOWER_LIFECYCLE_HOOKS` to `true`.
+
+
+
+### Specifying update commands
+
+The commands are specified using docker container labels, with
+`com.centurylinklabs.watchtower.lifecycle.pre-update-command` for the *pre-update*
+command and `com.centurylinklabs.watchtower.lifecycle.post-update` for the
+*post-update* command.
+
+These labels can be declared as instructions in a Dockerfile:
+
+```docker
+LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="/dump-data.sh"
+LABEL com.centurylinklabs.watchtower.lifecycle.post-update="/restore-data.sh"
+```
+
+Or be specified as part of the `docker run` command line:
+
+```bash
+docker run -d \
+ --label=com.centurylinklabs.watchtower.lifecycle.pre-update="/dump-data.sh" \
+ --label=com.centurylinklabs.watchtower.lifecycle.post-update="/restore-data.sh" \
+ someimage
+```
+
+### Execution failure
+
+The failure of a command to execute, identified by an exit code different than
+0, will not prevent watchtower from updating the container. Only an error
+log statement containing the exit code will be reported.
\ No newline at end of file
diff --git a/docs/linked-containers.md b/docs/linked-containers.md
new file mode 100644
index 0000000..6960b5b
--- /dev/null
+++ b/docs/linked-containers.md
@@ -0,0 +1,3 @@
+Watchtower will detect if there are links between any of the running containers and ensures that things are stopped/started in a way that won't break any of the links. If an update is detected for one of the dependencies in a group of linked containers, watchtower will stop and start all of the containers in the correct order so that the application comes back up correctly.
+
+For example, imagine you were running a _mysql_ container and a _wordpress_ container which had been linked to the _mysql_ container. If watchtower were to detect that the _mysql_ container required an update, it would first shut down the linked _wordpress_ container followed by the _mysql_ container. When restarting the containers it would handle _mysql_ first and then _wordpress_ to ensure that the link continued to work.
\ No newline at end of file
diff --git a/docs/notifications.md b/docs/notifications.md
new file mode 100644
index 0000000..5741566
--- /dev/null
+++ b/docs/notifications.md
@@ -0,0 +1,110 @@
+
+# Notifications
+
+Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging system, [logrus](http://github.com/sirupsen/logrus).
+The types of notifications to send are passed via the comma-separated option `--notifications` (or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
+
+- `email` to send notifications via e-mail
+- `slack` to send notifications through a Slack webhook
+- `msteams` to send notifications via MSTeams webhook
+- `gotify` to send notifications via Gotify
+
+## Settings
+
+- `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info` or `debug`.
+
+## Available services
+
+### Email
+
+To receive notifications by email, the following command-line options, or their corresponding environment variables, can be set:
+
+- `--notification-email-from` (env. `WATCHTOWER_NOTIFICATION_EMAIL_FROM`): The e-mail address from which notifications will be sent.
+- `--notification-email-to` (env. `WATCHTOWER_NOTIFICATION_EMAIL_TO`): The e-mail address to which notifications will be sent.
+- `--notification-email-server` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER`): The SMTP server to send e-mails through.
+- `--notification-email-server-tls-skip-verify` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY`): Do not verify the TLS certificate of the mail server. This should be used only for testing.
+- `--notification-email-server-port` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT`): The port used to connect to the SMTP server to send e-mails through. Defaults to `25`.
+- `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.
+- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with.
+- `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds.
+
+Example:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -e WATCHTOWER_NOTIFICATIONS=email \
+ -e WATCHTOWER_NOTIFICATION_EMAIL_FROM=fromaddress@gmail.com \
+ -e WATCHTOWER_NOTIFICATION_EMAIL_TO=toaddress@gmail.com \
+ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com \
+ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=fromaddress@gmail.com \
+ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \
+ -e WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2 \
+ containrrr/watchtower
+```
+
+### Slack
+If watchtower is monitoring the same Docker daemon under which the watchtower container itself is running (i.e. if you volume-mounted _/var/run/docker.sock_ into the watchtower container) then it has the ability to update itself. If a new version of the _containrrr/watchtower_ image is pushed to the Docker Hub, your watchtower will pull down the new image and restart itself automatically.
+
+To receive notifications in Slack, add `slack` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
+
+Additionally, you should set the Slack webhook URL using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable.
+
+By default, watchtower will send messages under the name `watchtower`, you can customize this string through the `--notification-slack-identifier` option or the `WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER` environment variable.
+
+Other, optional, variables include:
+
+- `--notification-slack-channel` (env. `WATCHTOWER_NOTIFICATION_SLACK_CHANNEL`): A string which overrides the webhook's default channel. Example: #my-custom-channel.
+- `--notification-slack-icon-emoji` (env. `WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI`): An [emoji code](https://www.webpagefx.com/tools/emoji-cheat-sheet/) string to use in place of the default icon.
+- `--notification-slack-icon-url` (env. `WATCHTOWER_NOTIFICATION_SLACK_ICON_URL`): An icon image URL string to use in place of the default icon.
+
+Example:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -e WATCHTOWER_NOTIFICATIONS=slack \
+ -e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL="https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy" \
+ -e WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=watchtower-server-1 \
+ -e WATCHTOWER_NOTIFICATION_SLACK_CHANNEL=#my-custom-channel \
+ -e WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI=:whale: \
+ -e WATCHTOWER_NOTIFICATION_SLACK_ICON_URL= \
+ containrrr/watchtower
+```
+
+### Microsoft Teams
+
+To receive notifications in MSTeams channel, add `msteams` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
+
+Additionally, you should set the MSTeams webhook URL using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable.
+
+MSTeams notifier could send keys/values filled by `log.WithField` or `log.WithFields` as MSTeams message facts. To enable this feature add `--notification-msteams-data` flag or set `WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true` environment variable.
+
+Example:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -e WATCHTOWER_NOTIFICATIONS=msteams \
+ -e WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL="https://outlook.office.com/webhook/xxxxxxxx@xxxxxxx/IncomingWebhook/yyyyyyyy/zzzzzzzzzz" \
+ -e WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true \
+ containrrr/watchtower
+```
+
+### Gotify
+
+To push a notification to your Gotify instance, register a Gotify app and specify the Gotify URL and app token:
+
+
+```bash
+docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -e WATCHTOWER_NOTIFICATIONS=gotify \
+ -e WATCHTOWER_NOTIFICATION_GOTIFY_URL="https://my.gotify.tld/" \
+ -e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN="SuperSecretToken" \
+ containrrr/watchtower
+```
diff --git a/docs/remote-hosts.md b/docs/remote-hosts.md
new file mode 100644
index 0000000..e08fbd3
--- /dev/null
+++ b/docs/remote-hosts.md
@@ -0,0 +1,18 @@
+By default, watchtower is set-up to monitor the local Docker daemon (the same daemon running the watchtower container itself). However, it is possible to configure watchtower to monitor a remote Docker endpoint. When starting the watchtower container you can specify a remote Docker endpoint with either the `--host` flag or the `DOCKER_HOST` environment variable:
+
+```bash
+docker run -d \
+ --name watchtower \
+ containrrr/watchtower --host "tcp://10.0.1.2:2375"
+```
+
+or
+
+```bash
+docker run -d \
+ --name watchtower \
+ -e DOCKER_HOST="tcp://10.0.1.2:2375" \
+ containrrr/watchtower
+```
+
+Note in both of the examples above that it is unnecessary to mount the _/var/run/docker.sock_ into the watchtower container.
\ No newline at end of file
diff --git a/docs/secure-connections.md b/docs/secure-connections.md
new file mode 100644
index 0000000..0c2a900
--- /dev/null
+++ b/docs/secure-connections.md
@@ -0,0 +1,14 @@
+Watchtower is also capable of connecting to Docker endpoints which are protected by SSL/TLS. If you've used _docker-machine_ to provision your remote Docker host, you simply need to volume mount the certificates generated by _docker-machine_ into the watchtower container and optionally specify `--tlsverify` flag.
+
+The _docker-machine_ certificates for a particular host can be located by executing the `docker-machine env` command for the desired host (note the values for the `DOCKER_HOST` and `DOCKER_CERT_PATH` environment variables that are returned from this command). The directory containing the certificates for the remote host needs to be mounted into the watchtower container at _/etc/ssl/docker_.
+
+With the certificates mounted into the watchtower container you need to specify the `--tlsverify` flag to enable verification of the certificate:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -e DOCKER_HOST=$DOCKER_HOST \
+ -e DOCKER_CERT_PATH=/etc/ssl/docker \
+ -v $DOCKER_CERT_PATH:/etc/ssl/docker \
+ containrrr/watchtower --tlsverify
+```
diff --git a/docs/stop-signals.md b/docs/stop-signals.md
new file mode 100644
index 0000000..f4b4f1d
--- /dev/null
+++ b/docs/stop-signals.md
@@ -0,0 +1,14 @@
+When watchtower detects that a running container needs to be updated it will stop the container by sending it a SIGTERM signal.
+If your container should be shutdown with a different signal you can communicate this to watchtower by setting a label named _com.centurylinklabs.watchtower.stop-signal_ with the value of the desired signal.
+
+This label can be coded directly into your image by using the `LABEL` instruction in your Dockerfile:
+
+```docker
+LABEL com.centurylinklabs.watchtower.stop-signal="SIGHUP"
+```
+
+Or, it can be specified as part of the `docker run` command line:
+
+```bash
+docker run -d --label=com.centurylinklabs.watchtower.stop-signal=SIGHUP someimage
+```
diff --git a/docs/usage-overview.md b/docs/usage-overview.md
new file mode 100644
index 0000000..f74a9a7
--- /dev/null
+++ b/docs/usage-overview.md
@@ -0,0 +1,56 @@
+Watchtower is itself packaged as a Docker container so installation is as simple as pulling the `containrrr/watchtower` image. If you are using ARM based architecture, pull the appropriate `containrrr/watchtower:armhf-` image from the [containrrr Docker Hub](https://hub.docker.com/r/containrrr/watchtower/tags/).
+
+Since the watchtower code needs to interact with the Docker API in order to monitor the running containers, you need to mount _/var/run/docker.sock_ into the container with the `-v` flag when you run it.
+
+Run the `watchtower` container with the following command:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ containrrr/watchtower
+```
+
+If pulling images from private Docker registries, supply registry authentication credentials with the environment variables `REPO_USER` and `REPO_PASS`
+or by mounting the host's docker config file into the container (at the root of the container filesystem `/`).
+
+Passing environment variables:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -e REPO_USER=username \
+ -e REPO_PASS=password \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ containrrr/watchtower container_to_watch --debug
+```
+
+Also check out [this Stack Overflow answer](https://stackoverflow.com/a/30494145/7872793) for more options on how to pass environment variables.
+
+Mounting the host's docker config file:
+
+```bash
+docker run -d \
+ --name watchtower \
+ -v /home//.docker/config.json:/config.json \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ containrrr/watchtower container_to_watch --debug
+```
+
+If you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container from a private repo at Docker Hub and monitors it with watchtower. Note the command argument changing the interval to 30s rather than the default 5 minutes.
+
+```json
+version: "3"
+services:
+ cavo:
+ image: index.docker.io//:
+ ports:
+ - "443:3443"
+ - "80:3080"
+ watchtower:
+ image: containrrr/watchtower
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - /root/.docker/config.json:/config.json
+ command: --interval 30
+```
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 0d7ae0e..1690237 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78
github.com/Microsoft/go-winio v0.4.12
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412
- github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973
+ github.com/beorn7/perks v1.0.0
github.com/brysgo/gomock_ginkgo v0.0.0-20180512161304-be2c1b0e4111
github.com/containerd/containerd v1.2.6 // indirect
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808
@@ -23,7 +23,6 @@ require (
github.com/gogo/protobuf v1.2.1
github.com/golang/mock v1.1.1
github.com/golang/protobuf v1.3.1
- github.com/google/go-cmp v0.2.0 // indirect
github.com/gorilla/mux v1.7.0
github.com/hashicorp/go-memdb v1.0.0 // indirect
github.com/hashicorp/go-version v1.1.0
@@ -44,24 +43,24 @@ require (
github.com/opencontainers/selinux v1.2.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pmezard/go-difflib v1.0.0
- github.com/prometheus/client_golang v0.9.2
+ github.com/prometheus/client_golang v0.9.3
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
- github.com/prometheus/common v0.2.0
- github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872
+ github.com/prometheus/common v0.4.0
+ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/sirupsen/logrus v1.4.1
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
+ github.com/spf13/viper v1.4.0
github.com/stretchr/objx v0.1.1
github.com/stretchr/testify v1.3.0
github.com/theupdateframework/notary v0.6.1
- github.com/urfave/cli v1.20.0
github.com/vbatts/tar-split v0.11.1 // indirect
golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692
- golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b
+ golang.org/x/net v0.0.0-20190522155817-f3200d17e092
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e
- golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
+ google.golang.org/appengine v1.4.0 // indirect
google.golang.org/genproto v0.0.0-20190401181712-f467c93bbac2
- google.golang.org/grpc v1.19.1
+ google.golang.org/grpc v1.21.0
gotest.tools v2.2.0+incompatible // indirect
)
diff --git a/go.sum b/go.sum
index b0415fc..2107c46 100644
--- a/go.sum
+++ b/go.sum
@@ -4,22 +4,34 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/brysgo/gomock_ginkgo v0.0.0-20180512161304-be2c1b0e4111 h1:gRfsoKtF1tba+hVsNgo7OKG7a35hBK30ouOTHPgqFf8=
github.com/brysgo/gomock_ginkgo v0.0.0-20180512161304-be2c1b0e4111/go.mod h1:H1ipqq0hhUWJgVeQ5dbUe/C8YptJrE/VGDQp9bI+qTo=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/containerd v1.2.6 h1:K38ZSAA9oKSrX3iFNY+4SddZ8hH1TCMCerc8NHfcKBQ=
github.com/containerd/containerd v1.2.6/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
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/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=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/cli v0.0.0-20190327152802-57b27434ea29 h1:ciaXDHaWQda0nvevWqcjtXX/buQY3e0lga1vq8Batq0=
github.com/docker/cli v0.0.0-20190327152802-57b27434ea29/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
@@ -40,21 +52,30 @@ github.com/docker/swarmkit v1.12.0 h1:vcbNXevt9xOod0miQxkp9WZ70IsOCe8geXkmFnXP2e
github.com/docker/swarmkit v1.12.0/go.mod h1:n3Z4lIEl7g261ptkGDBcYi/3qBMDl9csaAhwi2MPejs=
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=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+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/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.0.0 h1:K1O4N2VPndZiTrdH3lmmf5bemr9Xw81KjVwhReIUjTQ=
github.com/hashicorp/go-memdb v1.0.0/go.mod h1:I6dKdmYhZqU0RJSheVEWgTNWdVQH5QvTgIUQ0t/t32M=
@@ -62,6 +83,8 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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=
@@ -70,6 +93,7 @@ github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 h1:jKUP9TQ0c7X3
github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22/go.mod h1:u0Jo4f2dNlTJeeOywkM6bLwxq6gC3pZ9rEFHn3AhTdk=
github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 h1:+kBG/8rjCa6vxJZbUjAiE4MQmBEBYc8nLEb51frnvBY=
github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -77,13 +101,21 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+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-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/pkcs11 v0.0.0-20190401114359-553cfdd26aaa/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -98,6 +130,8 @@ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59P
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.2.1 h1:Svlc+L67YcjN4K2bqD8Wlw9jtMlmZ+1FEGn6zsm8am0=
github.com/opencontainers/selinux v1.2.1/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
+github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -106,25 +140,44 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
+github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872 h1:0aNv3xC7DmQoy1/x1sMh18g+fihWW68LL13i8ao9kl4=
github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -133,9 +186,15 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
-github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
-github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpcEPSzR8z6g=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692 h1:GRhHqDOgeDr6QDTtq9gn2O4iKvm5dsbfqD/TXb0KLX0=
@@ -143,14 +202,19 @@ golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692/go.mod h1:WFFai1msRO1wXaE
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b h1:/zjbcJPEGAyu6Is/VBOALsgdi4z9+kz/Vtdm6S+beD0=
golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b/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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -160,16 +224,19 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
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=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -178,12 +245,20 @@ google.golang.org/genproto v0.0.0-20190401181712-f467c93bbac2/go.mod h1:VzzqZJRn
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
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=
+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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go
similarity index 80%
rename from actions/actions_suite_test.go
rename to internal/actions/actions_suite_test.go
index 850fd4e..76d2be5 100644
--- a/actions/actions_suite_test.go
+++ b/internal/actions/actions_suite_test.go
@@ -2,14 +2,15 @@ package actions_test
import (
"errors"
+ "github.com/containrrr/watchtower/internal/actions"
"testing"
"time"
- "github.com/containrrr/watchtower/actions"
- "github.com/containrrr/watchtower/container"
- "github.com/containrrr/watchtower/container/mocks"
+ "github.com/containrrr/watchtower/pkg/container"
+ "github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/docker/docker/api/types"
+ t "github.com/containrrr/watchtower/pkg/types"
cli "github.com/docker/docker/client"
. "github.com/onsi/ginkgo"
@@ -32,9 +33,10 @@ var _ = Describe("the actions package", func() {
})
BeforeEach(func() {
client = mockClient{
- api: dockerClient,
- pullImages: false,
- TestData: &TestData{},
+ api: dockerClient,
+ pullImages: false,
+ removeVolumes: false,
+ TestData: &TestData{},
}
})
@@ -62,8 +64,9 @@ var _ = Describe("the actions package", func() {
When("given multiple containers", func() {
BeforeEach(func() {
client = mockClient{
- api: dockerClient,
- pullImages: false,
+ api: dockerClient,
+ pullImages: false,
+ removeVolumes: false,
TestData: &TestData{
NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{
@@ -89,8 +92,9 @@ var _ = Describe("the actions package", func() {
When("deciding whether to cleanup images", func() {
BeforeEach(func() {
client = mockClient{
- api: dockerClient,
- pullImages: false,
+ api: dockerClient,
+ pullImages: false,
+ removeVolumes: false,
TestData: &TestData{
Containers: []container.Container{
createMockContainer(
@@ -134,9 +138,10 @@ func createMockContainer(id string, name string, image string, created time.Time
}
type mockClient struct {
- TestData *TestData
- api cli.CommonAPIClient
- pullImages bool
+ TestData *TestData
+ api cli.CommonAPIClient
+ pullImages bool
+ removeVolumes bool
}
type TestData struct {
@@ -145,7 +150,7 @@ type TestData struct {
Containers []container.Container
}
-func (client mockClient) ListContainers(f container.Filter) ([]container.Container, error) {
+func (client mockClient) ListContainers(f t.Filter) ([]container.Container, error) {
return client.TestData.Containers, nil
}
@@ -155,7 +160,7 @@ func (client mockClient) StopContainer(c container.Container, d time.Duration) e
}
return nil
}
-func (client mockClient) StartContainer(c container.Container) error {
+func (client mockClient) StartContainer(c container.Container) (string, error) {
panic("Not implemented")
}
@@ -168,6 +173,14 @@ func (client mockClient) RemoveImage(c container.Container) error {
return nil
}
+func (client mockClient) GetContainer(containerID string) (container.Container, error) {
+ return container.Container{}, nil
+}
+
+func (client mockClient) ExecuteCommand(containerID string, command string) error {
+ return nil
+}
+
func (client mockClient) IsContainerStale(c container.Container) (bool, error) {
panic("Not implemented")
}
diff --git a/actions/check.go b/internal/actions/check.go
similarity index 92%
rename from actions/check.go
rename to internal/actions/check.go
index 16fd42e..8574300 100644
--- a/actions/check.go
+++ b/internal/actions/check.go
@@ -11,7 +11,7 @@ import (
log "github.com/sirupsen/logrus"
- "github.com/containrrr/watchtower/container"
+ "github.com/containrrr/watchtower/pkg/container"
)
// CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the
@@ -50,7 +50,7 @@ func cleanupExcessWatchtowers(containers []container.Container, client container
continue
}
- if cleanup == true {
+ if cleanup {
if err := client.RemoveImage(c); err != nil {
// logging the original here as we're just returning a count
logrus.Error(err)
@@ -79,6 +79,6 @@ func createErrorIfAnyHaveOccurred(c int, i int) error {
}
func awaitDockerClient() {
- log.Debug("Sleeping for a seconds to ensure the docker api client has been properly initialized.")
+ log.Debug("Sleeping for a second to ensure the docker api client has been properly initialized.")
time.Sleep(1 * time.Second)
}
diff --git a/internal/actions/update.go b/internal/actions/update.go
new file mode 100644
index 0000000..5763017
--- /dev/null
+++ b/internal/actions/update.go
@@ -0,0 +1,155 @@
+package actions
+
+import (
+ "github.com/containrrr/watchtower/internal/util"
+ "github.com/containrrr/watchtower/pkg/container"
+ log "github.com/sirupsen/logrus"
+)
+
+// Update looks at the running Docker containers to see if any of the images
+// used to start those containers have been updated. If a change is detected in
+// any of the images, the associated containers are stopped and restarted with
+// the new image.
+func Update(client container.Client, params UpdateParams) error {
+ log.Debug("Checking containers for updated images")
+
+ containers, err := client.ListContainers(params.Filter)
+ if err != nil {
+ return err
+ }
+
+ for i, container := range containers {
+ stale, err := client.IsContainerStale(container)
+ if err != nil {
+ log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
+ log.Debug(err)
+ stale = false
+ }
+ containers[i].Stale = stale
+ }
+
+ containers, err = container.SortByDependencies(containers)
+ if err != nil {
+ return err
+ }
+
+ checkDependencies(containers)
+
+ if params.MonitorOnly {
+ return nil
+ }
+
+ stopContainersInReversedOrder(containers, client, params)
+ restartContainersInSortedOrder(containers, client, params)
+
+ return nil
+}
+
+func stopContainersInReversedOrder(containers []container.Container, client container.Client, params UpdateParams) {
+ for i := len(containers) - 1; i >= 0; i-- {
+ stopStaleContainer(containers[i], client, params)
+ }
+}
+
+func stopStaleContainer(container container.Container, client container.Client, params UpdateParams) {
+ if container.IsWatchtower() {
+ log.Debugf("This is the watchtower container %s", container.Name())
+ return
+ }
+
+ if !container.Stale {
+ return
+ }
+
+ executePreUpdateCommand(client, container)
+
+ if err := client.StopContainer(container, params.Timeout); err != nil {
+ log.Error(err)
+ }
+}
+
+func restartContainersInSortedOrder(containers []container.Container, client container.Client, params UpdateParams) {
+ for _, container := range containers {
+ if !container.Stale {
+ continue
+ }
+ restartStaleContainer(container, client, params)
+ }
+}
+
+func restartStaleContainer(container container.Container, client container.Client, params UpdateParams) {
+ // Since we can't shutdown a watchtower container immediately, we need to
+ // start the new one while the old one is still running. This prevents us
+ // from re-using the same container name so we first rename the current
+ // instance so that the new one can adopt the old name.
+ if container.IsWatchtower() {
+ if err := client.RenameContainer(container, util.RandName()); err != nil {
+ log.Error(err)
+ return
+ }
+ }
+
+ if !params.NoRestart {
+ if newContainerID, err := client.StartContainer(container); err != nil {
+ log.Error(err)
+ } else if container.Stale && params.LifecycleHooks {
+ executePostUpdateCommand(client, newContainerID)
+ }
+ }
+
+ if params.Cleanup {
+ if err := client.RemoveImage(container); err != nil {
+ log.Error(err)
+ }
+ }
+}
+
+func checkDependencies(containers []container.Container) {
+
+ for i, parent := range containers {
+ if parent.ToRestart() {
+ continue
+ }
+
+ LinkLoop:
+ for _, linkName := range parent.Links() {
+ for _, child := range containers {
+ if child.Name() == linkName && child.ToRestart() {
+ containers[i].Linked = true
+ break LinkLoop
+ }
+ }
+ }
+ }
+}
+
+func executePreUpdateCommand(client container.Client, container container.Container) {
+
+ command := container.GetLifecyclePreUpdateCommand()
+ if len(command) == 0 {
+ log.Debug("No pre-update command supplied. Skipping")
+ }
+
+ log.Info("Executing pre-update command.")
+ if err := client.ExecuteCommand(container.ID(), command); err != nil {
+ log.Error(err)
+ }
+}
+
+func executePostUpdateCommand(client container.Client, newContainerID string) {
+ newContainer, err := client.GetContainer(newContainerID)
+ if err != nil {
+ log.Error(err)
+ return
+ }
+
+ command := newContainer.GetLifecyclePostUpdateCommand()
+ if len(command) == 0 {
+ log.Debug("No post-update command supplied. Skipping")
+ }
+
+ log.Info("Executing post-update command.")
+ if err := client.ExecuteCommand(newContainerID, command); err != nil {
+ log.Error(err)
+ }
+}
diff --git a/internal/actions/update_params.go b/internal/actions/update_params.go
new file mode 100644
index 0000000..ff586c6
--- /dev/null
+++ b/internal/actions/update_params.go
@@ -0,0 +1,16 @@
+package actions
+
+import (
+ t "github.com/containrrr/watchtower/pkg/types"
+ "time"
+)
+
+// UpdateParams contains all different options available to alter the behavior of the Update func
+type UpdateParams struct {
+ Filter t.Filter
+ Cleanup bool
+ NoRestart bool
+ Timeout time.Duration
+ MonitorOnly bool
+ LifecycleHooks bool
+}
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
new file mode 100644
index 0000000..6e9ea55
--- /dev/null
+++ b/internal/flags/flags.go
@@ -0,0 +1,311 @@
+package flags
+
+import (
+ "os"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+// DockerAPIMinVersion is the minimum version of the docker api required to
+// use watchtower
+const DockerAPIMinVersion string = "1.24"
+
+// RegisterDockerFlags that are used directly by the docker api client
+func RegisterDockerFlags(rootCmd *cobra.Command) {
+ flags := rootCmd.PersistentFlags()
+ flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to")
+ flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
+ flags.StringP("api-version", "a", viper.GetString("DOCKER_API_VERSION"), "api version to use by docker client")
+}
+
+// RegisterSystemFlags that are used by watchtower to modify the program flow
+func RegisterSystemFlags(rootCmd *cobra.Command) {
+ flags := rootCmd.PersistentFlags()
+ flags.IntP(
+ "interval",
+ "i",
+ viper.GetInt("WATCHTOWER_POLL_INTERVAL"),
+ "poll interval (in seconds)")
+
+ flags.StringP("schedule",
+ "s",
+ viper.GetString("WATCHTOWER_SCHEDULE"),
+ "the cron expression which defines when to update")
+
+ flags.DurationP("stop-timeout",
+ "t",
+ viper.GetDuration("WATCHTOWER_TIMEOUT"),
+ "timeout before a container is forcefully stopped")
+
+ flags.BoolP(
+ "no-pull",
+ "",
+ viper.GetBool("WATCHTOWER_NO_PULL"),
+ "do not pull any new images")
+
+ flags.BoolP(
+ "no-restart",
+ "",
+ viper.GetBool("WATCHTOWER_NO_RESTART"),
+ "do not restart any containers")
+
+ flags.BoolP(
+ "cleanup",
+ "c",
+ viper.GetBool("WATCHTOWER_CLEANUP"),
+ "remove previously used images after updating")
+
+ flags.BoolP(
+ "remove-volumes",
+ "",
+ viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
+ "remove attached volumes before updating")
+
+ flags.BoolP(
+ "label-enable",
+ "e",
+ viper.GetBool("WATCHTOWER_LABEL_ENABLE"),
+ "watch containers where the com.centurylinklabs.watchtower.enable label is true")
+
+ flags.BoolP(
+ "debug",
+ "d",
+ viper.GetBool("WATCHTOWER_DEBUG"),
+ "enable debug mode with verbose logging")
+
+ flags.BoolP(
+ "monitor-only",
+ "m",
+ viper.GetBool("WATCHTOWER_MONITOR_ONLY"),
+ "Will only monitor for new images, not update the containers")
+
+ flags.BoolP(
+ "run-once",
+ "R",
+ viper.GetBool("WATCHTOWER_RUN_ONCE"),
+ "Run once now and exit")
+
+ flags.BoolP(
+ "include-stopped",
+ "S",
+ viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"),
+ "Will also include created and exited containers")
+
+ flags.BoolP(
+ "enable-lifecycle-hooks",
+ "",
+ viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"),
+ "Enable the execution of commands triggered by pre- and post-update lifecycle hooks")
+}
+
+// RegisterNotificationFlags that are used by watchtower to send notifications
+func RegisterNotificationFlags(rootCmd *cobra.Command) {
+ flags := rootCmd.PersistentFlags()
+
+ flags.StringSliceP(
+ "notifications",
+ "n",
+ viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
+ " notification types to send (valid: email, slack, msteams, gotify)")
+
+ flags.StringP(
+ "notifications-level",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
+ "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
+
+ flags.StringP(
+ "notification-email-from",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
+ "Address to send notification emails from")
+
+ flags.StringP(
+ "notification-email-to",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
+ "Address to send notification emails to")
+
+ flags.IntP(
+ "notification-email-delay",
+ "",
+ viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
+ "Delay before sending notifications, expressed in seconds")
+
+ flags.StringP(
+ "notification-email-server",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
+ "SMTP server to send notification emails through")
+
+ flags.IntP(
+ "notification-email-server-port",
+ "",
+ viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
+ "SMTP server port to send notification emails through")
+
+ flags.BoolP(
+ "notification-email-server-tls-skip-verify",
+ "",
+ viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
+ `
+Controls whether watchtower verifies the SMTP server's certificate chain and host name.
+Should only be used for testing.
+`)
+
+ flags.StringP(
+ "notification-email-server-user",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
+ "SMTP server user for sending notifications")
+
+ flags.StringP(
+ "notification-email-server-password",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
+ "SMTP server password for sending notifications")
+
+ flags.StringP(
+ "notification-slack-hook-url",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
+ "The Slack Hook URL to send notifications to")
+
+ flags.StringP(
+ "notification-slack-identifier",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
+ "A string which will be used to identify the messages coming from this watchtower instance")
+
+ flags.StringP(
+ "notification-slack-channel",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
+ "A string which overrides the webhook's default channel. Example: #my-custom-channel")
+
+ flags.StringP(
+ "notification-slack-icon-emoji",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
+ "An emoji code string to use in place of the default icon")
+
+ flags.StringP(
+ "notification-slack-icon-url",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
+ "An icon image URL string to use in place of the default icon")
+
+ flags.StringP(
+ "notification-msteams-hook",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
+ "The MSTeams WebHook URL to send notifications to")
+
+ flags.BoolP(
+ "notification-msteams-data",
+ "",
+ viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
+ "The MSTeams notifier will try to extract log entry fields as MSTeams message facts")
+
+ flags.StringP(
+ "notification-gotify-url",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
+ "The Gotify URL to send notifications to")
+ flags.StringP(
+ "notification-gotify-token",
+ "",
+ viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
+ "The Gotify Application required to query the Gotify API")
+}
+
+// SetDefaults provides default values for environment variables
+func SetDefaults() {
+ viper.AutomaticEnv()
+ viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock")
+ viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion)
+ viper.SetDefault("WATCHTOWER_POLL_INTERVAL", 300)
+ viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10)
+ viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{})
+ viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info")
+ viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25)
+ viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
+}
+
+// EnvConfig translates the command-line options into environment variables
+// that will initialize the api client
+func EnvConfig(cmd *cobra.Command) error {
+ var err error
+ var host string
+ var tls bool
+ var version string
+
+ flags := cmd.PersistentFlags()
+
+ if host, err = flags.GetString("host"); err != nil {
+ return err
+ }
+ if tls, err = flags.GetBool("tlsverify"); err != nil {
+ return err
+ }
+ if version, err = flags.GetString("api-version"); err != nil {
+ return err
+ }
+ if err = setEnvOptStr("DOCKER_HOST", host); err != nil {
+ return err
+ }
+ if err = setEnvOptBool("DOCKER_TLS_VERIFY", tls); err != nil {
+ return err
+ }
+ if err = setEnvOptStr("DOCKER_API_VERSION", version); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ReadFlags reads common flags used in the main program flow of watchtower
+func ReadFlags(cmd *cobra.Command) (bool, bool, bool, time.Duration) {
+ flags := cmd.PersistentFlags()
+
+ var err error
+ var cleanup bool
+ var noRestart bool
+ var monitorOnly bool
+ var timeout time.Duration
+
+ if cleanup, err = flags.GetBool("cleanup"); err != nil {
+ log.Fatal(err)
+ }
+ if noRestart, err = flags.GetBool("no-restart"); err != nil {
+ log.Fatal(err)
+ }
+ if monitorOnly, err = flags.GetBool("monitor-only"); err != nil {
+ log.Fatal(err)
+ }
+ if timeout, err = flags.GetDuration("stop-timeout"); err != nil {
+ log.Fatal(err)
+ }
+
+ return cleanup, noRestart, monitorOnly, timeout
+}
+
+func setEnvOptStr(env string, opt string) error {
+ if opt == "" || opt == os.Getenv(env) {
+ return nil
+ }
+ err := os.Setenv(env, opt)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func setEnvOptBool(env string, opt bool) error {
+ if opt {
+ return setEnvOptStr(env, "1")
+ }
+ return nil
+}
diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go
new file mode 100644
index 0000000..ac57b30
--- /dev/null
+++ b/internal/flags/flags_test.go
@@ -0,0 +1,39 @@
+package flags
+
+import (
+ "os"
+ "testing"
+
+ "github.com/spf13/cobra"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestEnvConfig_Defaults(t *testing.T) {
+ cmd := new(cobra.Command)
+ SetDefaults()
+ RegisterDockerFlags(cmd)
+
+ err := EnvConfig(cmd)
+ require.NoError(t, err)
+
+ assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST"))
+ assert.Equal(t, "", os.Getenv("DOCKER_TLS_VERIFY"))
+ assert.Equal(t, DockerAPIMinVersion, os.Getenv("DOCKER_API_VERSION"))
+}
+
+func TestEnvConfig_Custom(t *testing.T) {
+ cmd := new(cobra.Command)
+ SetDefaults()
+ RegisterDockerFlags(cmd)
+
+ err := cmd.ParseFlags([]string{"--host", "some-custom-docker-host", "--tlsverify", "--api-version", "1.99"})
+ require.NoError(t, err)
+
+ err = EnvConfig(cmd)
+ require.NoError(t, err)
+
+ assert.Equal(t, "some-custom-docker-host", os.Getenv("DOCKER_HOST"))
+ assert.Equal(t, "1", os.Getenv("DOCKER_TLS_VERIFY"))
+ assert.Equal(t, "1.99", os.Getenv("DOCKER_API_VERSION"))
+}
diff --git a/internal/util/rand_name.go b/internal/util/rand_name.go
new file mode 100644
index 0000000..76f6a3f
--- /dev/null
+++ b/internal/util/rand_name.go
@@ -0,0 +1,15 @@
+package util
+
+import "math/rand"
+
+var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+
+// RandName Generates a random, 32-character, Docker-compatible container name.
+func RandName() string {
+ b := make([]rune, 32)
+ for i := range b {
+ b[i] = letters[rand.Intn(len(letters))]
+ }
+
+ return string(b)
+}
diff --git a/container/util.go b/internal/util/util.go
similarity index 54%
rename from container/util.go
rename to internal/util/util.go
index 3db01d1..08c88bc 100644
--- a/container/util.go
+++ b/internal/util/util.go
@@ -1,6 +1,7 @@
-package container
+package util
-func sliceEqual(s1, s2 []string) bool {
+// SliceEqual compares two slices and checks whether they have equal content
+func SliceEqual(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
}
@@ -14,7 +15,8 @@ func sliceEqual(s1, s2 []string) bool {
return true
}
-func sliceSubtract(a1, a2 []string) []string {
+// SliceSubtract subtracts the content of slice a2 from slice a1
+func SliceSubtract(a1, a2 []string) []string {
a := []string{}
for _, e1 := range a1 {
@@ -35,7 +37,8 @@ func sliceSubtract(a1, a2 []string) []string {
return a
}
-func stringMapSubtract(m1, m2 map[string]string) map[string]string {
+// StringMapSubtract subtracts the content of structmap m2 from structmap m1
+func StringMapSubtract(m1, m2 map[string]string) map[string]string {
m := map[string]string{}
for k1, v1 := range m1 {
@@ -51,7 +54,8 @@ func stringMapSubtract(m1, m2 map[string]string) map[string]string {
return m
}
-func structMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
+// StructMapSubtract subtracts the content of structmap m2 from structmap m1
+func StructMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
m := map[string]struct{}{}
for k1, v1 := range m1 {
diff --git a/container/util_test.go b/internal/util/util_test.go
similarity index 86%
rename from container/util_test.go
rename to internal/util/util_test.go
index 8c4eef9..a6dd657 100644
--- a/container/util_test.go
+++ b/internal/util/util_test.go
@@ -1,17 +1,15 @@
-package container
+package util
import (
- "testing"
"github.com/stretchr/testify/assert"
+ "testing"
)
-
-
func TestSliceEqual_True(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "c"}
- result := sliceEqual(s1, s2)
+ result := SliceEqual(s1, s2)
assert.True(t, result)
}
@@ -20,7 +18,7 @@ func TestSliceEqual_DifferentLengths(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "c", "d"}
- result := sliceEqual(s1, s2)
+ result := SliceEqual(s1, s2)
assert.False(t, result)
}
@@ -29,7 +27,7 @@ func TestSliceEqual_DifferentContents(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "d"}
- result := sliceEqual(s1, s2)
+ result := SliceEqual(s1, s2)
assert.False(t, result)
}
@@ -38,7 +36,7 @@ func TestSliceSubtract(t *testing.T) {
a1 := []string{"a", "b", "c"}
a2 := []string{"a", "c"}
- result := sliceSubtract(a1, a2)
+ result := SliceSubtract(a1, a2)
assert.Equal(t, []string{"b"}, result)
assert.Equal(t, []string{"a", "b", "c"}, a1)
assert.Equal(t, []string{"a", "c"}, a2)
@@ -48,7 +46,7 @@ func TestStringMapSubtract(t *testing.T) {
m1 := map[string]string{"a": "a", "b": "b", "c": "sea"}
m2 := map[string]string{"a": "a", "c": "c"}
- result := stringMapSubtract(m1, m2)
+ result := StringMapSubtract(m1, m2)
assert.Equal(t, map[string]string{"b": "b", "c": "sea"}, result)
assert.Equal(t, map[string]string{"a": "a", "b": "b", "c": "sea"}, m1)
assert.Equal(t, map[string]string{"a": "a", "c": "c"}, m2)
@@ -59,7 +57,7 @@ func TestStructMapSubtract(t *testing.T) {
m1 := map[string]struct{}{"a": x, "b": x, "c": x}
m2 := map[string]struct{}{"a": x, "c": x}
- result := structMapSubtract(m1, m2)
+ result := StructMapSubtract(m1, m2)
assert.Equal(t, map[string]struct{}{"b": x}, result)
assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1)
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)
diff --git a/main.go b/main.go
index a9b35c7..9f8a012 100644
--- a/main.go
+++ b/main.go
@@ -1,39 +1,8 @@
-package main // import "github.com/containrrr/watchtower"
+package main
import (
- "os"
- "os/signal"
- "syscall"
- "time"
-
- "strconv"
-
- "github.com/containrrr/watchtower/actions"
- cliApp "github.com/containrrr/watchtower/app"
- "github.com/containrrr/watchtower/container"
- "github.com/containrrr/watchtower/notifications"
- "github.com/robfig/cron"
+ "github.com/containrrr/watchtower/cmd"
log "github.com/sirupsen/logrus"
- "github.com/urfave/cli"
-)
-
-// DockerAPIMinVersion is the version of the docker API, which is minimally required by
-// watchtower. Currently we require at least API 1.24 and therefore Docker 1.12 or later.
-const DockerAPIMinVersion string = "1.24"
-
-var version = "master"
-var commit = "unknown"
-var date = "unknown"
-
-var (
- client container.Client
- scheduleSpec string
- cleanup bool
- noRestart bool
- monitorOnly bool
- enableLabel bool
- notifier *notifications.Notifier
- timeout time.Duration
)
func init() {
@@ -41,170 +10,5 @@ func init() {
}
func main() {
- app := cli.NewApp()
- InitApp(app)
- cliApp.SetupCliFlags(app)
-
- if err := app.Run(os.Args); err != nil {
- log.Fatal(err)
- }
-}
-
-// InitApp initializes urfave app metadata and sets up entrypoints
-func InitApp(app *cli.App) {
- app.Name = "watchtower"
- app.Version = version + " - " + commit + " - " + date
- app.Usage = "Automatically update running Docker containers"
- app.Before = before
- app.Action = start
-}
-
-func before(c *cli.Context) error {
- if c.GlobalBool("debug") {
- log.SetLevel(log.DebugLevel)
- }
-
- pollingSet := c.IsSet("interval")
- cronSet := c.IsSet("schedule")
- cronLen := len(c.String("schedule"))
-
- if pollingSet && cronSet && cronLen > 0 {
- log.Fatal("Only schedule or interval can be defined, not both.")
- } else if cronSet && cronLen > 0 {
- scheduleSpec = c.String("schedule")
- } else {
- scheduleSpec = "@every " + strconv.Itoa(c.Int("interval")) + "s"
- }
-
- readFlags(c)
-
- if timeout < 0 {
- log.Fatal("Please specify a positive value for timeout value.")
- }
- enableLabel = c.GlobalBool("label-enable")
-
- // configure environment vars for client
- err := envConfig(c)
- if err != nil {
- return err
- }
-
- client = container.NewClient(!c.GlobalBool("no-pull"))
- notifier = notifications.NewNotifier(c)
-
- return nil
-}
-
-func start(c *cli.Context) error {
- names := c.Args()
- filter := container.BuildFilter(names, enableLabel)
-
- if c.GlobalBool("run-once") {
- log.Info("Running a one time update.")
- runUpdatesWithNotifications(filter)
- os.Exit(1)
- return nil
- }
-
- if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup); err != nil {
- log.Fatal(err)
- }
-
- runUpgradesOnSchedule(filter)
- os.Exit(1)
- return nil
-}
-
-func runUpgradesOnSchedule(filter container.Filter) error {
- tryLockSem := make(chan bool, 1)
- tryLockSem <- true
-
- cron := cron.New()
- err := cron.AddFunc(
- scheduleSpec,
- func() {
- select {
- case v := <-tryLockSem:
- defer func() { tryLockSem <- v }()
- runUpdatesWithNotifications(filter)
- default:
- log.Debug("Skipped another update already running.")
- }
-
- nextRuns := cron.Entries()
- if len(nextRuns) > 0 {
- log.Debug("Scheduled next run: " + nextRuns[0].Next.String())
- }
- })
-
- if err != nil {
- return err
- }
-
- log.Debug("Starting Watchtower and scheduling first run: " + cron.Entries()[0].Schedule.Next(time.Now()).String())
- cron.Start()
-
- // Graceful shut-down on SIGINT/SIGTERM
- interrupt := make(chan os.Signal, 1)
- signal.Notify(interrupt, os.Interrupt)
- signal.Notify(interrupt, syscall.SIGTERM)
-
- <-interrupt
- cron.Stop()
- log.Info("Waiting for running update to be finished...")
- <-tryLockSem
- return nil
-}
-
-func runUpdatesWithNotifications(filter container.Filter) {
- notifier.StartNotification()
- updateParams := actions.UpdateParams{
- Filter: filter,
- Cleanup: cleanup,
- NoRestart: noRestart,
- Timeout: timeout,
- MonitorOnly: monitorOnly,
- }
- err := actions.Update(client, updateParams)
- if err != nil {
- log.Println(err)
- }
- notifier.SendNotification()
-}
-
-func setEnvOptStr(env string, opt string) error {
- if opt == "" || opt == os.Getenv(env) {
- return nil
- }
- err := os.Setenv(env, opt)
- if err != nil {
- return err
- }
- return nil
-}
-
-func setEnvOptBool(env string, opt bool) error {
- if opt == true {
- return setEnvOptStr(env, "1")
- }
- return nil
-}
-
-// envConfig translates the command-line options into environment variables
-// that will initialize the api client
-func envConfig(c *cli.Context) error {
- var err error
-
- err = setEnvOptStr("DOCKER_HOST", c.GlobalString("host"))
- err = setEnvOptBool("DOCKER_TLS_VERIFY", c.GlobalBool("tlsverify"))
- err = setEnvOptStr("DOCKER_API_VERSION", DockerAPIMinVersion)
-
- return err
-}
-
-func readFlags(c *cli.Context) {
- cleanup = c.GlobalBool("cleanup")
- noRestart = c.GlobalBool("no-restart")
- monitorOnly = c.GlobalBool("monitor-only")
- timeout = c.GlobalDuration("stop-timeout")
+ cmd.Execute()
}
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..9656c6d
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,23 @@
+site_name: Watchtower
+site_url: http://containrrr.github.io/watchtower/
+repo_url: https://github.com/containrrr/watchtower/
+theme:
+ name: 'material'
+markdown_extensions:
+ - toc:
+ permalink: True
+ separator: "_"
+nav:
+ - 'Home': 'index.md'
+ - 'Introduction': 'introduction.md'
+ - 'Usage overview': 'usage-overview.md'
+ - 'Arguments': 'arguments.md'
+ - 'Notifications': 'notifications.md'
+ - 'Container selection': 'container-selection.md'
+ - 'Credential helpers': 'credential-helpers.md'
+ - 'Linked containers': 'linked-containers.md'
+ - 'Remote hosts': 'remote-hosts.md'
+ - 'Secure connections': 'secure-connections.md'
+ - 'Stop signals': 'stop-signals.md'
+plugins:
+ - search
\ No newline at end of file
diff --git a/notifications/slack.go b/notifications/slack.go
deleted file mode 100644
index 08f8d3b..0000000
--- a/notifications/slack.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package notifications
-
-import (
- "github.com/johntdyer/slackrus"
- log "github.com/sirupsen/logrus"
- "github.com/urfave/cli"
-)
-
-const (
- slackType = "slack"
-)
-
-type slackTypeNotifier struct {
- slackrus.SlackrusHook
-}
-
-func newSlackNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifier {
- n := &slackTypeNotifier{
- SlackrusHook: slackrus.SlackrusHook{
- HookURL: c.GlobalString("notification-slack-hook-url"),
- Username: c.GlobalString("notification-slack-identifier"),
- Channel: c.GlobalString("notification-slack-channel"),
- IconEmoji: c.GlobalString("notification-slack-icon-emoji"),
- IconURL: c.GlobalString("notification-slack-icon-url"),
- AcceptedLevels: acceptedLogLevels,
- },
- }
-
- log.AddHook(n)
-
- return n
-}
-
-func (s *slackTypeNotifier) StartNotification() {}
-
-func (s *slackTypeNotifier) SendNotification() {}
diff --git a/container/client.go b/pkg/container/client.go
similarity index 52%
rename from container/client.go
rename to pkg/container/client.go
index 94f790e..0dc22db 100644
--- a/container/client.go
+++ b/pkg/container/client.go
@@ -1,29 +1,34 @@
package container
import (
+ "bytes"
"fmt"
"io/ioutil"
+ "strings"
"time"
+ t "github.com/containrrr/watchtower/pkg/types"
"github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
-const (
- defaultStopSignal = "SIGTERM"
-)
+const defaultStopSignal = "SIGTERM"
// A Client is the interface through which watchtower interacts with the
// Docker API.
type Client interface {
- ListContainers(Filter) ([]Container, error)
+ ListContainers(t.Filter) ([]Container, error)
+ GetContainer(containerID string) (Container, error)
StopContainer(Container, time.Duration) error
- StartContainer(Container) error
+ StartContainer(Container) (string, error)
RenameContainer(Container, string) error
IsContainerStale(Container) (bool, error)
+ ExecuteCommand(containerID string, command string) error
RemoveImage(Container) error
}
@@ -33,48 +38,56 @@ type Client interface {
// * DOCKER_HOST the docker-engine host to send api requests to
// * DOCKER_TLS_VERIFY whether to verify tls certificates
// * DOCKER_API_VERSION the minimum docker api version to work with
-func NewClient(pullImages bool) Client {
+func NewClient(pullImages bool, includeStopped bool, removeVolumes bool) Client {
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
if err != nil {
log.Fatalf("Error instantiating Docker client: %s", err)
}
- return dockerClient{api: cli, pullImages: pullImages}
+ return dockerClient{
+ api: cli,
+ pullImages: pullImages,
+ removeVolumes: removeVolumes,
+ includeStopped: includeStopped,
+ }
}
type dockerClient struct {
- api dockerclient.CommonAPIClient
- pullImages bool
+ api dockerclient.CommonAPIClient
+ pullImages bool
+ removeVolumes bool
+ includeStopped bool
}
-func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
+func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
cs := []Container{}
bg := context.Background()
- log.Debug("Retrieving running containers")
+ if client.includeStopped {
+ log.Debug("Retrieving containers including stopped and exited")
+ } else {
+ log.Debug("Retrieving running containers")
+ }
- runningContainers, err := client.api.ContainerList(
+ filter := client.createListFilter()
+ containers, err := client.api.ContainerList(
bg,
- types.ContainerListOptions{})
+ types.ContainerListOptions{
+ Filters: filter,
+ })
if err != nil {
return nil, err
}
- for _, runningContainer := range runningContainers {
- containerInfo, err := client.api.ContainerInspect(bg, runningContainer.ID)
+ for _, runningContainer := range containers {
+
+ c, err := client.GetContainer(runningContainer.ID)
if err != nil {
return nil, err
}
- imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
- if err != nil {
- return nil, err
- }
-
- c := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
-
if fn(c) {
cs = append(cs, c)
}
@@ -83,6 +96,35 @@ func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
return cs, nil
}
+func (client dockerClient) createListFilter() filters.Args {
+ filterArgs := filters.NewArgs()
+ filterArgs.Add("status", "running")
+
+ if client.includeStopped {
+ filterArgs.Add("status", "created")
+ filterArgs.Add("status", "exited")
+ }
+
+ return filterArgs
+}
+
+func (client dockerClient) GetContainer(containerID string) (Container, error) {
+ bg := context.Background()
+
+ containerInfo, err := client.api.ContainerInspect(bg, containerID)
+ if err != nil {
+ return Container{}, err
+ }
+
+ imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
+ if err != nil {
+ return Container{}, err
+ }
+
+ container := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
+ return container, nil
+}
+
func (client dockerClient) StopContainer(c Container, timeout time.Duration) error {
bg := context.Background()
signal := c.StopSignal()
@@ -90,34 +132,35 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
signal = defaultStopSignal
}
- log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal)
-
- if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil {
- return err
+ if c.IsRunning() {
+ log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal)
+ if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil {
+ return err
+ }
}
- // Wait for container to exit, but proceed anyway after the timeout elapses
- client.waitForStop(c, timeout)
+ // TODO: This should probably be checked.
+ _ = client.waitForStopOrTimeout(c, timeout)
if c.containerInfo.HostConfig.AutoRemove {
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", c.ID())
} else {
log.Debugf("Removing container %s", c.ID())
- if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: false}); err != nil {
+ if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
return err
}
}
// Wait for container to be removed. In this case an error is a good thing
- if err := client.waitForStop(c, timeout); err == nil {
+ if err := client.waitForStopOrTimeout(c, timeout); err == nil {
return fmt.Errorf("Container %s (%s) could not be removed", c.Name(), c.ID())
}
return nil
}
-func (client dockerClient) StartContainer(c Container) error {
+func (client dockerClient) StartContainer(c Container) (string, error) {
bg := context.Background()
config := c.runtimeConfig()
hostConfig := c.hostConfig()
@@ -137,38 +180,46 @@ func (client dockerClient) StartContainer(c Container) error {
name := c.Name()
log.Infof("Creating %s", name)
- creation, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, name)
+ createdContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, name)
if err != nil {
- return err
+ return "", err
}
if !(hostConfig.NetworkMode.IsHost()) {
for k := range simpleNetworkConfig.EndpointsConfig {
- err = client.api.NetworkDisconnect(bg, k, creation.ID, true)
+ err = client.api.NetworkDisconnect(bg, k, createdContainer.ID, true)
if err != nil {
- return err
+ return "", err
}
}
for k, v := range networkConfig.EndpointsConfig {
- err = client.api.NetworkConnect(bg, k, creation.ID, v)
+ err = client.api.NetworkConnect(bg, k, createdContainer.ID, v)
if err != nil {
- return err
+ return "", err
}
}
}
- log.Debugf("Starting container %s (%s)", name, creation.ID)
+ if !c.IsRunning() {
+ return createdContainer.ID, nil
+ }
- err = client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
+ return createdContainer.ID, client.doStartContainer(bg, c, createdContainer)
+
+}
+
+func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
+ name := c.Name()
+
+ log.Debugf("Starting container %s (%s)", name, creation.ID)
+ err := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
if err != nil {
return err
}
-
return nil
-
}
func (client dockerClient) RenameContainer(c Container, newName string) error {
@@ -207,7 +258,9 @@ func (client dockerClient) IsContainerStale(c Container) (bool, error) {
defer response.Close()
// the pull request will be aborted prematurely unless the response is read
- _, err = ioutil.ReadAll(response)
+ if _, err = ioutil.ReadAll(response); err != nil {
+ log.Error(err)
+ }
}
newImageInfo, _, err := client.api.ImageInspectWithRaw(bg, imageName)
@@ -231,7 +284,68 @@ func (client dockerClient) RemoveImage(c Container) error {
return err
}
-func (client dockerClient) waitForStop(c Container, waitTime time.Duration) error {
+func (client dockerClient) ExecuteCommand(containerID string, command string) error {
+ bg := context.Background()
+
+ // Create the exec
+ execConfig := types.ExecConfig{
+ Tty: true,
+ Detach: false,
+ Cmd: []string{"sh", "-c", command},
+ }
+
+ exec, err := client.api.ContainerExecCreate(bg, containerID, execConfig)
+ if err != nil {
+ return err
+ }
+
+ response, attachErr := client.api.ContainerExecAttach(bg, exec.ID, types.ExecStartCheck{
+ Tty: true,
+ Detach: false,
+ })
+ if attachErr != nil {
+ log.Errorf("Failed to extract command exec logs: %v", attachErr)
+ }
+
+ // Run the exec
+ execStartCheck := types.ExecStartCheck{Detach: false, Tty: true}
+ err = client.api.ContainerExecStart(bg, exec.ID, execStartCheck)
+ if err != nil {
+ return err
+ }
+
+ var execOutput string
+ if attachErr == nil {
+ defer response.Close()
+ var writer bytes.Buffer
+ written, err := writer.ReadFrom(response.Reader)
+ if err != nil {
+ log.Error(err)
+ } else if written > 0 {
+ execOutput = 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)
+ if err != nil {
+ return err
+ }
+
+ if execInspect.ExitCode > 0 {
+ log.Errorf("Command exited with code %v.", execInspect.ExitCode)
+ log.Error(execOutput)
+ } else {
+ if len(execOutput) > 0 {
+ log.Infof("Command output:\n%v", execOutput)
+ }
+ }
+
+ return nil
+}
+
+func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error {
bg := context.Background()
timeout := time.After(waitTime)
diff --git a/container/container.go b/pkg/container/container.go
similarity index 81%
rename from container/container.go
rename to pkg/container/container.go
index b32d4aa..09e4225 100644
--- a/container/container.go
+++ b/pkg/container/container.go
@@ -2,6 +2,7 @@ package container
import (
"fmt"
+ "github.com/containrrr/watchtower/internal/util"
"strconv"
"strings"
@@ -9,13 +10,6 @@ import (
dockercontainer "github.com/docker/docker/api/types/container"
)
-const (
- watchtowerLabel = "com.centurylinklabs.watchtower"
- signalLabel = "com.centurylinklabs.watchtower.stop-signal"
- enableLabel = "com.centurylinklabs.watchtower.enable"
- zodiacLabel = "com.centurylinklabs.zodiac.original-image"
-)
-
// NewContainer returns a new Container instance instantiated with the
// specified ContainerInfo and ImageInfo structs.
func NewContainer(containerInfo *types.ContainerJSON, imageInfo *types.ImageInspect) *Container {
@@ -27,7 +21,8 @@ func NewContainer(containerInfo *types.ContainerJSON, imageInfo *types.ImageInsp
// Container represents a running Docker container.
type Container struct {
- Stale bool
+ Linked bool
+ Stale bool
containerInfo *types.ContainerJSON
imageInfo *types.ImageInspect
@@ -38,6 +33,13 @@ func (c Container) ID() string {
return c.containerInfo.ID
}
+// IsRunning returns a boolean flag indicating whether or not the current
+// container is running. The status is determined by the value of the
+// container's "State.Running" property.
+func (c Container) IsRunning() bool {
+ return c.containerInfo.State.Running
+}
+
// Name returns the Docker container name.
func (c Container) Name() string {
return c.containerInfo.Name
@@ -54,7 +56,7 @@ func (c Container) ImageID() string {
// "latest" tag is assumed.
func (c Container) ImageName() string {
// Compatibility w/ Zodiac deployments
- imageName, ok := c.containerInfo.Config.Labels[zodiacLabel]
+ imageName, ok := c.getLabelValue(zodiacLabel)
if !ok {
imageName = c.containerInfo.Config.Image
}
@@ -69,7 +71,7 @@ func (c Container) ImageName() string {
// Enabled returns the value of the container enabled label and if the label
// was set.
func (c Container) Enabled() (bool, bool) {
- rawBool, ok := c.containerInfo.Config.Labels[enableLabel]
+ rawBool, ok := c.getLabelValue(enableLabel)
if !ok {
return false, false
}
@@ -97,6 +99,12 @@ func (c Container) Links() []string {
return links
}
+// ToRestart return whether the container should be restarted, either because
+// is stale or linked to another stale container.
+func (c Container) ToRestart() bool {
+ return c.Stale || c.Linked
+}
+
// IsWatchtower returns a boolean flag indicating whether or not the current
// container is the watchtower container itself. The watchtower container is
// identified by the presence of the "com.centurylinklabs.watchtower" label in
@@ -109,11 +117,7 @@ func (c Container) IsWatchtower() bool {
// container's metadata. If the container has not specified a custom stop
// signal, the empty string "" is returned.
func (c Container) StopSignal() string {
- if val, ok := c.containerInfo.Config.Labels[signalLabel]; ok {
- return val
- }
-
- return ""
+ return c.getLabelValueOrEmpty(signalLabel)
}
// Ideally, we'd just be able to take the ContainerConfig from the old container
@@ -139,19 +143,19 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
config.User = ""
}
- if sliceEqual(config.Cmd, imageConfig.Cmd) {
+ if util.SliceEqual(config.Cmd, imageConfig.Cmd) {
config.Cmd = nil
}
- if sliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
+ if util.SliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
config.Entrypoint = nil
}
- config.Env = sliceSubtract(config.Env, imageConfig.Env)
+ config.Env = util.SliceSubtract(config.Env, imageConfig.Env)
- config.Labels = stringMapSubtract(config.Labels, imageConfig.Labels)
+ config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
- config.Volumes = structMapSubtract(config.Volumes, imageConfig.Volumes)
+ config.Volumes = util.StructMapSubtract(config.Volumes, imageConfig.Volumes)
// subtract ports exposed in image from container
for k := range config.ExposedPorts {
@@ -181,10 +185,3 @@ func (c Container) hostConfig() *dockercontainer.HostConfig {
return hostConfig
}
-
-// ContainsWatchtowerLabel takes a map of labels and values and tells
-// the consumer whether it contains a valid watchtower instance label
-func ContainsWatchtowerLabel(labels map[string]string) bool {
- val, ok := labels[watchtowerLabel]
- return ok && val == "true"
-}
diff --git a/container/container_test.go b/pkg/container/container_test.go
similarity index 82%
rename from container/container_test.go
rename to pkg/container/container_test.go
index 2543976..9e1b213 100644
--- a/container/container_test.go
+++ b/pkg/container/container_test.go
@@ -1,7 +1,7 @@
package container
import (
- "github.com/containrrr/watchtower/container/mocks"
+ "github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
cli "github.com/docker/docker/client"
@@ -17,15 +17,15 @@ func TestContainer(t *testing.T) {
var _ = Describe("the container", func() {
Describe("the client", func() {
+ var docker *cli.Client
var client Client
BeforeSuite(func() {
server := mocks.NewMockAPIServer()
- c, _ := cli.NewClientWithOpts(
+ docker, _ = cli.NewClientWithOpts(
cli.WithHost(server.URL),
- cli.WithHTTPClient(server.Client(),
- ))
+ cli.WithHTTPClient(server.Client()))
client = dockerClient{
- api: c,
+ api: docker,
pullImages: false,
}
})
@@ -41,7 +41,7 @@ var _ = Describe("the container", func() {
})
When("listing containers with a filter matching nothing", func() {
It("should return an empty array", func() {
- filter := filterByNames([]string { "lollercoaster"}, noFilter)
+ filter := filterByNames([]string{"lollercoaster"}, noFilter)
containers, err := client.ListContainers(filter)
Expect(err).NotTo(HaveOccurred())
Expect(len(containers) == 0).To(BeTrue())
@@ -55,13 +55,25 @@ var _ = Describe("the container", func() {
Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest"))
})
})
+ When(`listing containers with the "include stopped" option`, func() {
+ It("should return both stopped and running containers", func() {
+ client = dockerClient{
+ api: docker,
+ pullImages: false,
+ includeStopped: true,
+ }
+ containers, err := client.ListContainers(noFilter)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(len(containers) > 0).To(BeTrue())
+ })
+ })
})
When("asked for metadata", func() {
var c *Container
BeforeEach(func() {
- c = mockContainerWithLabels(map[string]string {
+ c = mockContainerWithLabels(map[string]string{
"com.centurylinklabs.watchtower.enable": "true",
- "com.centurylinklabs.watchtower": "true",
+ "com.centurylinklabs.watchtower": "true",
})
})
It("should return its name on calls to .Name()", func() {
@@ -84,7 +96,7 @@ var _ = Describe("the container", func() {
Expect(exists).NotTo(BeFalse())
})
It("should return false, true if present but not true on calls to .Enabled()", func() {
- c = mockContainerWithLabels(map[string]string{ "com.centurylinklabs.watchtower.enable": "false" })
+ c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"})
enabled, exists := c.Enabled()
Expect(enabled).To(BeFalse())
@@ -93,7 +105,7 @@ var _ = Describe("the container", func() {
Expect(exists).NotTo(BeFalse())
})
It("should return false, false if not present on calls to .Enabled()", func() {
- c = mockContainerWithLabels(map[string]string{ "lol": "false" })
+ c = mockContainerWithLabels(map[string]string{"lol": "false"})
enabled, exists := c.Enabled()
Expect(enabled).To(BeFalse())
@@ -102,7 +114,7 @@ var _ = Describe("the container", func() {
Expect(exists).NotTo(BeTrue())
})
It("should return false, false if present but not parsable .Enabled()", func() {
- c = mockContainerWithLabels(map[string]string{ "com.centurylinklabs.watchtower.enable": "falsy" })
+ c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"})
enabled, exists := c.Enabled()
Expect(enabled).To(BeFalse())
@@ -116,12 +128,12 @@ var _ = Describe("the container", func() {
Expect(isWatchtower).To(BeTrue())
})
It("should return false if the label is present but set to false", func() {
- c = mockContainerWithLabels(map[string]string{ "com.centurylinklabs.watchtower": "false" })
+ c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"})
isWatchtower := c.IsWatchtower()
Expect(isWatchtower).To(BeFalse())
})
It("should return false if the label is not present", func() {
- c = mockContainerWithLabels(map[string]string{ "funny.label": "false" })
+ c = mockContainerWithLabels(map[string]string{"funny.label": "false"})
isWatchtower := c.IsWatchtower()
Expect(isWatchtower).To(BeFalse())
})
diff --git a/container/filters.go b/pkg/container/filters.go
similarity index 58%
rename from container/filters.go
rename to pkg/container/filters.go
index 1b3fbbd..b4d4911 100644
--- a/container/filters.go
+++ b/pkg/container/filters.go
@@ -1,30 +1,20 @@
package container
-// A Filter is a prototype for a function that can be used to filter the
-// results from a call to the ListContainers() method on the Client.
-type Filter func(FilterableContainer) bool
-
-// A FilterableContainer is the interface which is used to filter
-// containers.
-type FilterableContainer interface {
- Name() string
- IsWatchtower() bool
- Enabled() (bool, bool)
-}
+import t "github.com/containrrr/watchtower/pkg/types"
// WatchtowerContainersFilter filters only watchtower containers
-func WatchtowerContainersFilter(c FilterableContainer) bool { return c.IsWatchtower() }
+func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }
// Filter no containers and returns all
-func noFilter(FilterableContainer) bool { return true }
+func noFilter(t.FilterableContainer) bool { return true }
// Filters containers which don't have a specified name
-func filterByNames(names []string, baseFilter Filter) Filter {
+func filterByNames(names []string, baseFilter t.Filter) t.Filter {
if len(names) == 0 {
return baseFilter
}
- return func(c FilterableContainer) bool {
+ return func(c t.FilterableContainer) bool {
for _, name := range names {
if (name == c.Name()) || (name == c.Name()[1:]) {
return baseFilter(c)
@@ -35,8 +25,8 @@ func filterByNames(names []string, baseFilter Filter) Filter {
}
// Filters out containers that don't have the 'enableLabel'
-func filterByEnableLabel(baseFilter Filter) Filter {
- return func(c FilterableContainer) bool {
+func filterByEnableLabel(baseFilter t.Filter) t.Filter {
+ return func(c t.FilterableContainer) bool {
// If label filtering is enabled, containers should only be considered
// if the label is specifically set.
_, ok := c.Enabled()
@@ -49,8 +39,8 @@ func filterByEnableLabel(baseFilter Filter) Filter {
}
// Filters out containers that have a 'enableLabel' and is set to disable.
-func filterByDisabledLabel(baseFilter Filter) Filter {
- return func(c FilterableContainer) bool {
+func filterByDisabledLabel(baseFilter t.Filter) t.Filter {
+ return func(c t.FilterableContainer) bool {
enabledLabel, ok := c.Enabled()
if ok && !enabledLabel {
// If the label has been set and it demands a disable
@@ -62,7 +52,7 @@ func filterByDisabledLabel(baseFilter Filter) Filter {
}
// BuildFilter creates the needed filter of containers
-func BuildFilter(names []string, enableLabel bool) Filter {
+func BuildFilter(names []string, enableLabel bool) t.Filter {
filter := noFilter
filter = filterByNames(names, filter)
if enableLabel {
diff --git a/container/filters_test.go b/pkg/container/filters_test.go
similarity index 98%
rename from container/filters_test.go
rename to pkg/container/filters_test.go
index 0db0a62..4118335 100644
--- a/container/filters_test.go
+++ b/pkg/container/filters_test.go
@@ -3,8 +3,8 @@ package container
import (
"testing"
+ "github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/stretchr/testify/assert"
- "github.com/containrrr/watchtower/container/mocks"
)
func TestWatchtowerContainersFilter(t *testing.T) {
diff --git a/pkg/container/metadata.go b/pkg/container/metadata.go
new file mode 100644
index 0000000..3ab9ec2
--- /dev/null
+++ b/pkg/container/metadata.go
@@ -0,0 +1,39 @@
+package container
+
+const (
+ watchtowerLabel = "com.centurylinklabs.watchtower"
+ signalLabel = "com.centurylinklabs.watchtower.stop-signal"
+ enableLabel = "com.centurylinklabs.watchtower.enable"
+ zodiacLabel = "com.centurylinklabs.zodiac.original-image"
+ preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
+ postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
+)
+
+// GetLifecyclePreUpdateCommand returns the pre-update command set in the container metadata or an empty string
+func (c Container) GetLifecyclePreUpdateCommand() string {
+ return c.getLabelValueOrEmpty(preUpdateLabel)
+}
+
+// GetLifecyclePostUpdateCommand returns the post-update command set in the container metadata or an empty string
+func (c Container) GetLifecyclePostUpdateCommand() string {
+ return c.getLabelValueOrEmpty(postUpdateLabel)
+}
+
+// ContainsWatchtowerLabel takes a map of labels and values and tells
+// the consumer whether it contains a valid watchtower instance label
+func ContainsWatchtowerLabel(labels map[string]string) bool {
+ val, ok := labels[watchtowerLabel]
+ return ok && val == "true"
+}
+
+func (c Container) getLabelValueOrEmpty(label string) string {
+ if val, ok := c.containerInfo.Config.Labels[label]; ok {
+ return val
+ }
+ return ""
+}
+
+func (c Container) getLabelValue(label string) (string, bool) {
+ val, ok := c.containerInfo.Config.Labels[label]
+ return val, ok
+}
diff --git a/container/mocks/ApiServer.go b/pkg/container/mocks/ApiServer.go
similarity index 79%
rename from container/mocks/ApiServer.go
rename to pkg/container/mocks/ApiServer.go
index 79290a2..82e05de 100644
--- a/container/mocks/ApiServer.go
+++ b/pkg/container/mocks/ApiServer.go
@@ -18,7 +18,11 @@ func NewMockAPIServer() *httptest.Server {
logrus.Debug("Mock server has received a HTTP call on ", r.URL)
var response = ""
- if isRequestFor("containers/json?limit=0", r) {
+ if isRequestFor("filters=%7B%22status%22%3A%7B%22running%22%3Atrue%7D%7D&limit=0", r) {
+ response = getMockJSONFromDisk("./mocks/data/containers.json")
+ } else if isRequestFor("filters=%7B%22status%22%3A%7B%22created%22%3Atrue%2C%22exited%22%3Atrue%2C%22running%22%3Atrue%7D%7D&limit=0", r) {
+ response = getMockJSONFromDisk("./mocks/data/containers.json")
+ } else if isRequestFor("containers/json?limit=0", r) {
response = getMockJSONFromDisk("./mocks/data/containers.json")
} else if isRequestFor("ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65", r) {
response = getMockJSONFromDisk("./mocks/data/container_stopped.json")
@@ -48,4 +52,3 @@ func getMockJSONFromDisk(relPath string) string {
}
return string(buf)
}
-
diff --git a/container/mocks/FilterableContainer.go b/pkg/container/mocks/FilterableContainer.go
similarity index 100%
rename from container/mocks/FilterableContainer.go
rename to pkg/container/mocks/FilterableContainer.go
diff --git a/container/mocks/data/container_running.json b/pkg/container/mocks/data/container_running.json
similarity index 100%
rename from container/mocks/data/container_running.json
rename to pkg/container/mocks/data/container_running.json
diff --git a/container/mocks/data/container_stopped.json b/pkg/container/mocks/data/container_stopped.json
similarity index 100%
rename from container/mocks/data/container_stopped.json
rename to pkg/container/mocks/data/container_stopped.json
diff --git a/container/mocks/data/containers.json b/pkg/container/mocks/data/containers.json
similarity index 99%
rename from container/mocks/data/containers.json
rename to pkg/container/mocks/data/containers.json
index a40cbf3..e2507bf 100644
--- a/container/mocks/data/containers.json
+++ b/pkg/container/mocks/data/containers.json
@@ -12,7 +12,7 @@
"Labels": {
"com.centurylinklabs.watchtower": "true"
},
- "State": "exited",
+ "State": "running",
"Status": "Exited (1) 6 days ago",
"HostConfig": {
"NetworkMode": "default"
diff --git a/container/mocks/data/image01.json b/pkg/container/mocks/data/image01.json
similarity index 100%
rename from container/mocks/data/image01.json
rename to pkg/container/mocks/data/image01.json
diff --git a/container/mocks/data/image02.json b/pkg/container/mocks/data/image02.json
similarity index 100%
rename from container/mocks/data/image02.json
rename to pkg/container/mocks/data/image02.json
diff --git a/container/sort.go b/pkg/container/sort.go
similarity index 100%
rename from container/sort.go
rename to pkg/container/sort.go
diff --git a/container/trust.go b/pkg/container/trust.go
similarity index 94%
rename from container/trust.go
rename to pkg/container/trust.go
index 92ab696..63b76a6 100644
--- a/container/trust.go
+++ b/pkg/container/trust.go
@@ -48,6 +48,10 @@ func EncodedEnvAuth(ref string) (string, error) {
// The docker config must be mounted on the container
func EncodedConfigAuth(ref string) (string, error) {
server, err := ParseServerAddress(ref)
+ if err != nil {
+ log.Errorf("Unable to parse the image ref %s", err)
+ return "", err
+ }
configDir := os.Getenv("DOCKER_CONFIG")
if configDir == "" {
configDir = "/"
@@ -58,7 +62,8 @@ func EncodedConfigAuth(ref string) (string, error) {
return "", err
}
credStore := CredentialsStore(*configFile)
- auth, err := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore
+ auth, _ := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore
+
if auth == (types.AuthConfig{}) {
log.Debugf("No credentials for %s in %s", server, configFile.Filename)
return "", nil
diff --git a/container/trust_test.go b/pkg/container/trust_test.go
similarity index 99%
rename from container/trust_test.go
rename to pkg/container/trust_test.go
index 6aa807b..7d2ac96 100644
--- a/container/trust_test.go
+++ b/pkg/container/trust_test.go
@@ -1,15 +1,11 @@
package container
import (
+ "github.com/stretchr/testify/assert"
"os"
"testing"
- "github.com/stretchr/testify/assert"
)
-
-
-
-
func TestEncodedEnvAuth_ShouldReturnAnErrorIfRepoEnvsAreUnset(t *testing.T) {
os.Unsetenv("REPO_USER")
os.Unsetenv("REPO_PASS")
diff --git a/notifications/email.go b/pkg/notifications/email.go
similarity index 71%
rename from notifications/email.go
rename to pkg/notifications/email.go
index 5f84ca5..b5ef979 100644
--- a/notifications/email.go
+++ b/pkg/notifications/email.go
@@ -3,21 +3,21 @@ package notifications
import (
"encoding/base64"
"fmt"
+ "github.com/spf13/cobra"
"net/smtp"
"os"
"time"
- "strconv"
-
+ t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
- "github.com/urfave/cli"
+ "strconv"
)
const (
emailType = "email"
)
-// Implements typeNotifier, logrus.Hook
+// Implements Notifier, logrus.Hook
// The default logrus email integration would have several issues:
// - It would send one email per log output
// - It would only send errors
@@ -29,18 +29,31 @@ type emailTypeNotifier struct {
tlsSkipVerify bool
entries []*log.Entry
logLevels []log.Level
+ delay time.Duration
}
-func newEmailNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifier {
+func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
+ flags := c.PersistentFlags()
+
+ from, _ := flags.GetString("notification-email-from")
+ to, _ := flags.GetString("notification-email-to")
+ server, _ := flags.GetString("notification-email-server")
+ user, _ := flags.GetString("notification-email-server-user")
+ password, _ := flags.GetString("notification-email-server-password")
+ port, _ := flags.GetInt("notification-email-server-port")
+ tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify")
+ delay, _ := flags.GetInt("notification-email-delay")
+
n := &emailTypeNotifier{
- From: c.GlobalString("notification-email-from"),
- To: c.GlobalString("notification-email-to"),
- Server: c.GlobalString("notification-email-server"),
- User: c.GlobalString("notification-email-server-user"),
- Password: c.GlobalString("notification-email-server-password"),
- Port: c.GlobalInt("notification-email-server-port"),
- tlsSkipVerify: c.GlobalBool("notification-email-server-tls-skip-verify"),
+ From: from,
+ To: to,
+ Server: server,
+ User: user,
+ Password: password,
+ Port: port,
+ tlsSkipVerify: tlsSkipVerify,
logLevels: acceptedLogLevels,
+ delay: time.Duration(delay) * time.Second,
}
log.AddHook(n)
@@ -60,7 +73,7 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
}
t := time.Now()
-
+
header := make(map[string]string)
header["From"] = e.From
header["To"] = e.To
@@ -107,9 +120,15 @@ func (e *emailTypeNotifier) StartNotification() {
}
func (e *emailTypeNotifier) SendNotification() {
- if e.entries != nil && len(e.entries) != 0 {
- e.sendEntries(e.entries)
+ if e.entries == nil || len(e.entries) <= 0 {
+ return
}
+
+ if e.delay > 0 {
+ time.Sleep(e.delay)
+ }
+
+ e.sendEntries(e.entries)
e.entries = nil
}
diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go
new file mode 100644
index 0000000..47ea884
--- /dev/null
+++ b/pkg/notifications/gotify.go
@@ -0,0 +1,101 @@
+package notifications
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ t "github.com/containrrr/watchtower/pkg/types"
+ log "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+)
+
+const (
+ gotifyType = "gotify"
+)
+
+type gotifyTypeNotifier struct {
+ gotifyURL string
+ gotifyAppToken string
+ logLevels []log.Level
+}
+
+func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
+ flags := c.PersistentFlags()
+
+ gotifyURL, _ := flags.GetString("notification-gotify-url")
+ if len(gotifyURL) < 1 {
+ log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.")
+ } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) {
+ log.Fatal("Gotify URL must start with \"http://\" or \"https://\"")
+ } else if strings.HasPrefix(gotifyURL, "http://") {
+ log.Warn("Using an HTTP url fpr Gotify is insecure")
+ }
+
+ gotifyToken, _ := flags.GetString("notification-gotify-token")
+ if len(gotifyToken) < 1 {
+ log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.")
+ }
+
+ n := &gotifyTypeNotifier{
+ gotifyURL: gotifyURL,
+ gotifyAppToken: gotifyToken,
+ logLevels: acceptedLogLevels,
+ }
+
+ log.AddHook(n)
+
+ return n
+}
+
+func (n *gotifyTypeNotifier) StartNotification() {}
+
+func (n *gotifyTypeNotifier) SendNotification() {}
+
+func (n *gotifyTypeNotifier) Levels() []log.Level {
+ return n.logLevels
+}
+
+func (n *gotifyTypeNotifier) getURL() string {
+ url := n.gotifyURL
+ if !strings.HasSuffix(url, "/") {
+ url += "/"
+ }
+ return url + "message?token=" + n.gotifyAppToken
+}
+
+func (n *gotifyTypeNotifier) Fire(entry *log.Entry) error {
+
+ go func() {
+ jsonBody, err := json.Marshal(gotifyMessage{
+ Message: "(" + entry.Level.String() + "): " + entry.Message,
+ Title: "Watchtower",
+ Priority: 0,
+ })
+ if err != nil {
+ fmt.Println("Failed to create JSON body for Gotify notification: ", err)
+ return
+ }
+
+ jsonBodyBuffer := bytes.NewBuffer([]byte(jsonBody))
+ resp, err := http.Post(n.getURL(), "application/json", jsonBodyBuffer)
+ if err != nil {
+ fmt.Println("Failed to send Gotify notification: ", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ fmt.Printf("Gotify notification returned %d HTTP status code", resp.StatusCode)
+ }
+
+ }()
+ return nil
+}
+
+type gotifyMessage struct {
+ Message string `json:"message"`
+ Title string `json:"title"`
+ Priority int `json:"priority"`
+}
diff --git a/notifications/msteams.go b/pkg/notifications/msteams.go
similarity index 90%
rename from notifications/msteams.go
rename to pkg/notifications/msteams.go
index 8bb9d7a..b356814 100644
--- a/notifications/msteams.go
+++ b/pkg/notifications/msteams.go
@@ -4,10 +4,11 @@ import (
"bytes"
"encoding/json"
"fmt"
+ "github.com/spf13/cobra"
"net/http"
+ t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
- "github.com/urfave/cli"
"io/ioutil"
)
@@ -21,17 +22,20 @@ type msTeamsTypeNotifier struct {
data bool
}
-func newMsTeamsNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifier {
+func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
- webHookURL := c.GlobalString("notification-msteams-hook")
+ flags := cmd.PersistentFlags()
+
+ webHookURL, _ := flags.GetString("notification-msteams-hook")
if len(webHookURL) <= 0 {
log.Fatal("Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.")
}
+ withData, _ := flags.GetBool("notification-msteams-data")
n := &msTeamsTypeNotifier{
levels: acceptedLogLevels,
webHookURL: webHookURL,
- data: c.GlobalBool("notification-msteams-data"),
+ data: withData,
}
log.AddHook(n)
diff --git a/notifications/notifier.go b/pkg/notifications/notifier.go
similarity index 76%
rename from notifications/notifier.go
rename to pkg/notifications/notifier.go
index 62e8ebc..2f25824 100644
--- a/notifications/notifier.go
+++ b/pkg/notifications/notifier.go
@@ -1,26 +1,25 @@
package notifications
import (
+ ty "github.com/containrrr/watchtower/pkg/types"
"github.com/johntdyer/slackrus"
log "github.com/sirupsen/logrus"
- "github.com/urfave/cli"
+ "github.com/spf13/cobra"
)
-type typeNotifier interface {
- StartNotification()
- SendNotification()
-}
-
// Notifier can send log output as notification to admins, with optional batching.
type Notifier struct {
- types []typeNotifier
+ types []ty.Notifier
}
// NewNotifier creates and returns a new Notifier, using global configuration.
-func NewNotifier(c *cli.Context) *Notifier {
+func NewNotifier(c *cobra.Command) *Notifier {
n := &Notifier{}
- logLevel, err := log.ParseLevel(c.GlobalString("notifications-level"))
+ f := c.PersistentFlags()
+
+ level, _ := f.GetString("notifications-level")
+ logLevel, err := log.ParseLevel(level)
if err != nil {
log.Fatalf("Notifications invalid log level: %s", err.Error())
}
@@ -28,9 +27,10 @@ func NewNotifier(c *cli.Context) *Notifier {
acceptedLogLevels := slackrus.LevelThreshold(logLevel)
// Parse types and create notifiers.
- types := c.GlobalStringSlice("notifications")
+ types, _ := f.GetStringSlice("notifications")
+
for _, t := range types {
- var tn typeNotifier
+ var tn ty.Notifier
switch t {
case emailType:
tn = newEmailNotifier(c, acceptedLogLevels)
@@ -38,6 +38,8 @@ func NewNotifier(c *cli.Context) *Notifier {
tn = newSlackNotifier(c, acceptedLogLevels)
case msTeamsType:
tn = newMsTeamsNotifier(c, acceptedLogLevels)
+ case gotifyType:
+ tn = newGotifyNotifier(c, acceptedLogLevels)
default:
log.Fatalf("Unknown notification type %q", t)
}
diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go
new file mode 100644
index 0000000..42b7915
--- /dev/null
+++ b/pkg/notifications/slack.go
@@ -0,0 +1,44 @@
+package notifications
+
+import (
+ t "github.com/containrrr/watchtower/pkg/types"
+ "github.com/johntdyer/slackrus"
+ log "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+)
+
+const (
+ slackType = "slack"
+)
+
+type slackTypeNotifier struct {
+ slackrus.SlackrusHook
+}
+
+func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
+ flags := c.PersistentFlags()
+
+ hookURL, _ := flags.GetString("notification-slack-hook-url")
+ userName, _ := flags.GetString("notification-slack-identifier")
+ channel, _ := flags.GetString("notification-slack-channel")
+ emoji, _ := flags.GetString("notification-slack-icon-emoji")
+ iconURL, _ := flags.GetString("notification-slack-icon-url")
+
+ n := &slackTypeNotifier{
+ SlackrusHook: slackrus.SlackrusHook{
+ HookURL: hookURL,
+ Username: userName,
+ Channel: channel,
+ IconEmoji: emoji,
+ IconURL: iconURL,
+ AcceptedLevels: acceptedLogLevels,
+ },
+ }
+
+ log.AddHook(n)
+ return n
+}
+
+func (s *slackTypeNotifier) StartNotification() {}
+
+func (s *slackTypeNotifier) SendNotification() {}
diff --git a/notifications/smtp.go b/pkg/notifications/smtp.go
similarity index 100%
rename from notifications/smtp.go
rename to pkg/notifications/smtp.go
diff --git a/notifications/util.go b/pkg/notifications/util.go
similarity index 100%
rename from notifications/util.go
rename to pkg/notifications/util.go
diff --git a/pkg/types/filter.go b/pkg/types/filter.go
new file mode 100644
index 0000000..514e4bd
--- /dev/null
+++ b/pkg/types/filter.go
@@ -0,0 +1,5 @@
+package types
+
+// A Filter is a prototype for a function that can be used to filter the
+// results from a call to the ListContainers() method on the Client.
+type Filter func(FilterableContainer) bool
diff --git a/pkg/types/filterable_container.go b/pkg/types/filterable_container.go
new file mode 100644
index 0000000..d89b910
--- /dev/null
+++ b/pkg/types/filterable_container.go
@@ -0,0 +1,9 @@
+package types
+
+// A FilterableContainer is the interface which is used to filter
+// containers.
+type FilterableContainer interface {
+ Name() string
+ IsWatchtower() bool
+ Enabled() (bool, bool)
+}
diff --git a/pkg/types/notifier.go b/pkg/types/notifier.go
new file mode 100644
index 0000000..c8d07d0
--- /dev/null
+++ b/pkg/types/notifier.go
@@ -0,0 +1,7 @@
+package types
+
+// Notifier is the interface that all notification services have in common
+type Notifier interface {
+ StartNotification()
+ SendNotification()
+}
diff --git a/scripts/lifecycle-tests.sh b/scripts/lifecycle-tests.sh
new file mode 100755
index 0000000..dd41823
--- /dev/null
+++ b/scripts/lifecycle-tests.sh
@@ -0,0 +1,208 @@
+#!/usr/bin/env bash
+
+set -e
+
+IMAGE=server
+CONTAINER=server
+LINKED_IMAGE=linked
+LINKED_CONTAINER=linked
+WATCHTOWER_INTERVAL=2
+
+function remove_container {
+ docker kill $1 >> /dev/null || true && docker rm -v $1 >> /dev/null || true
+}
+
+function cleanup {
+ # Do cleanup on exit or error
+ echo "Final cleanup"
+ sleep 2
+ remove_container $CONTAINER
+ remove_container $LINKED_CONTAINER
+ pkill -9 -f watchtower >> /dev/null || true
+}
+trap cleanup EXIT
+
+DEFAULT_WATCHTOWER="$(dirname "${BASH_SOURCE[0]}")/../watchtower"
+WATCHTOWER=$1
+WATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER}
+echo "watchtower path is $WATCHTOWER"
+
+##################################################################################
+##### PREPARATION ################################################################
+##################################################################################
+
+# Create Dockerfile template
+DOCKERFILE=$(cat << EOF
+FROM node:alpine
+
+LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="cat /opt/test/value.txt"
+LABEL com.centurylinklabs.watchtower.lifecycle.post-update="echo image > /opt/test/value.txt"
+
+ENV IMAGE_TIMESTAMP=TIMESTAMP
+
+WORKDIR /opt/test
+ENTRYPOINT ["/usr/local/bin/node", "/opt/test/server.js"]
+
+EXPOSE 8888
+
+RUN mkdir -p /opt/test && echo "default" > /opt/test/value.txt
+COPY server.js /opt/test/server.js
+EOF
+)
+
+# Create temporary directory to build docker image
+TMP_DIR="/tmp/watchtower-commands-test"
+mkdir -p $TMP_DIR
+
+# Create simple http server
+cat > $TMP_DIR/server.js << EOF
+const http = require("http");
+const fs = require("fs");
+
+http.createServer(function(request, response) {
+ const fileContent = fs.readFileSync("/opt/test/value.txt");
+ response.writeHead(200, {"Content-Type": "text/plain"});
+ response.write(fileContent);
+ response.end();
+}).listen(8888, () => { console.log('server is listening on 8888'); });
+EOF
+
+function builddocker {
+ TIMESTAMP=$(date +%s)
+ echo "Building image $TIMESTAMP"
+ echo "${DOCKERFILE/TIMESTAMP/$TIMESTAMP}" > $TMP_DIR/Dockerfile
+ docker build $TMP_DIR -t $IMAGE >> /dev/null
+}
+
+# Start watchtower
+echo "Starting watchtower"
+$WATCHTOWER -i $WATCHTOWER_INTERVAL --no-pull --stop-timeout 2s --enable-lifecycle-hooks $CONTAINER $LINKED_CONTAINER &
+sleep 3
+
+echo "#################################################################"
+echo "##### TEST CASE 1: Execute commands from base image"
+echo "#################################################################"
+
+# Build base image
+builddocker
+
+# Run container
+docker run -d -p 0.0.0.0:8888:8888 --name $CONTAINER $IMAGE:latest >> /dev/null
+sleep 1
+echo "Container $CONTAINER is runnning"
+
+# Test default value
+RESP=$(curl -s http://localhost:8888)
+if [ $RESP != "default" ]; then
+ echo "Default value of container response is invalid" 1>&2
+ exit 1
+fi
+
+# Build updated image to trigger watchtower update
+builddocker
+
+WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
+echo "Wait for $WAIT_AMOUNT seconds"
+sleep $WAIT_AMOUNT
+
+# Test value after post-update-command
+RESP=$(curl -s http://localhost:8888)
+if [[ $RESP != "image" ]]; then
+ echo "Value of container response is invalid. Expected: image. Actual: $RESP"
+ exit 1
+fi
+
+remove_container $CONTAINER
+
+echo "#################################################################"
+echo "##### TEST CASE 2: Execute commands from container and base image"
+echo "#################################################################"
+
+# Build base image
+builddocker
+
+# Run container
+docker run -d -p 0.0.0.0:8888:8888 \
+ --label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
+ --name $CONTAINER $IMAGE:latest >> /dev/null
+sleep 1
+echo "Container $CONTAINER is runnning"
+
+# Test default value
+RESP=$(curl -s http://localhost:8888)
+if [ $RESP != "default" ]; then
+ echo "Default value of container response is invalid" 1>&2
+ exit 1
+fi
+
+# Build updated image to trigger watchtower update
+builddocker
+
+WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
+echo "Wait for $WAIT_AMOUNT seconds"
+sleep $WAIT_AMOUNT
+
+# Test value after post-update-command
+RESP=$(curl -s http://localhost:8888)
+if [[ $RESP != "container" ]]; then
+ echo "Value of container response is invalid. Expected: container. Actual: $RESP"
+ exit 1
+fi
+
+remove_container $CONTAINER
+
+echo "#################################################################"
+echo "##### TEST CASE 3: Execute commands with a linked container"
+echo "#################################################################"
+
+# Tag the current image to keep a version for the linked container
+docker tag $IMAGE:latest $LINKED_IMAGE:latest
+
+# Build base image
+builddocker
+
+# Run container
+docker run -d -p 0.0.0.0:8888:8888 \
+ --label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
+ --name $CONTAINER $IMAGE:latest >> /dev/null
+docker run -d -p 0.0.0.0:8989:8888 \
+ --label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
+ --link $CONTAINER \
+ --name $LINKED_CONTAINER $LINKED_IMAGE:latest >> /dev/null
+sleep 1
+echo "Container $CONTAINER and $LINKED_CONTAINER are runnning"
+
+# Test default value
+RESP=$(curl -s http://localhost:8888)
+if [ $RESP != "default" ]; then
+ echo "Default value of container response is invalid" 1>&2
+ exit 1
+fi
+
+# Test default value for linked container
+RESP=$(curl -s http://localhost:8989)
+if [ $RESP != "default" ]; then
+ echo "Default value of linked container response is invalid" 1>&2
+ exit 1
+fi
+
+# Build updated image to trigger watchtower update
+builddocker
+
+WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
+echo "Wait for $WAIT_AMOUNT seconds"
+sleep $WAIT_AMOUNT
+
+# Test value after post-update-command
+RESP=$(curl -s http://localhost:8888)
+if [[ $RESP != "container" ]]; then
+ echo "Value of container response is invalid. Expected: container. Actual: $RESP"
+ exit 1
+fi
+
+# Test that linked container did not execute pre/post-update-command
+RESP=$(curl -s http://localhost:8989)
+if [[ $RESP != "default" ]]; then
+ echo "Value of linked container response is invalid. Expected: default. Actual: $RESP"
+ exit 1
+fi
\ No newline at end of file