Merge branch 'main' into healthcheck

This commit is contained in:
nils måsén 2023-09-16 17:28:44 +02:00 committed by GitHub
commit 246da4da68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 745 additions and 986 deletions

View file

@ -849,6 +849,15 @@
"contributions": [ "contributions": [
"doc" "doc"
] ]
},
{
"login": "testwill",
"name": "guangwu",
"avatar_url": "https://avatars.githubusercontent.com/u/8717479?v=4",
"profile": "https://github.com/testwill",
"contributions": [
"doc"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,
@ -857,5 +866,6 @@
"repoType": "github", "repoType": "github",
"repoHost": "https://github.com", "repoHost": "https://github.com",
"commitConvention": "none", "commitConvention": "none",
"skipCi": true "skipCi": true,
"commitType": "docs"
} }

View file

@ -31,7 +31,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup python - name: Setup python

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@ -37,7 +37,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@ -64,7 +64,7 @@ jobs:
with: with:
go-version: 1.18.x go-version: 1.18.x
- name: Build - name: Build
uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 #v3 uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
with: with:
version: v0.155.0 version: v0.155.0
args: --snapshot --skip-publish --debug args: --snapshot --skip-publish --debug

View file

@ -10,7 +10,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
@ -20,7 +20,7 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
@ -37,7 +37,7 @@ jobs:
- test - test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Publish to Docker Hub - name: Publish to Docker Hub
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14 uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
with: with:

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@ -40,7 +40,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@ -62,7 +62,7 @@ jobs:
TAG: ${{ github.event.release.tag_name }} TAG: ${{ github.event.release.tag_name }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@ -81,7 +81,7 @@ jobs:
password: ${{ secrets.BOT_GHCR_PAT }} password: ${{ secrets.BOT_GHCR_PAT }}
registry: ghcr.io registry: ghcr.io
- name: Build - name: Build
uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 #v3 uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
with: with:
version: v0.155.0 version: v0.155.0
args: --debug args: --debug

188
README.md
View file

@ -44,126 +44,126 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td align="center"><a href="http://codelica.com"><img src="https://avatars3.githubusercontent.com/u/386101?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Codelica" title="Tests">⚠️</a> <a href="#ideas-Codelica" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="http://codelica.com"><img src="https://avatars3.githubusercontent.com/u/386101?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Codelica" title="Tests">⚠️</a> <a href="#ideas-Codelica" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://kopfkrieg.org"><img src="https://avatars2.githubusercontent.com/u/5047813?v=4?s=100" width="100px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3AKopfKrieg" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=KopfKrieg" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://kopfkrieg.org"><img src="https://avatars2.githubusercontent.com/u/5047813?v=4?s=100" width="100px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3AKopfKrieg" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=KopfKrieg" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bdehamer"><img src="https://avatars1.githubusercontent.com/u/398027?v=4?s=100" width="100px;" alt="Brian DeHamer"/><br /><sub><b>Brian DeHamer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bdehamer" title="Code">💻</a> <a href="#maintenance-bdehamer" title="Maintenance">🚧</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/bdehamer"><img src="https://avatars1.githubusercontent.com/u/398027?v=4?s=100" width="100px;" alt="Brian DeHamer"/><br /><sub><b>Brian DeHamer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bdehamer" title="Code">💻</a> <a href="#maintenance-bdehamer" title="Maintenance">🚧</a></td>
<td align="center"><a href="https://github.com/rosscado"><img src="https://avatars1.githubusercontent.com/u/16578183?v=4?s=100" width="100px;" alt="Ross Cadogan"/><br /><sub><b>Ross Cadogan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rosscado" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/rosscado"><img src="https://avatars1.githubusercontent.com/u/16578183?v=4?s=100" width="100px;" alt="Ross Cadogan"/><br /><sub><b>Ross Cadogan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rosscado" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/stffabi"><img src="https://avatars0.githubusercontent.com/u/9464631?v=4?s=100" width="100px;" alt="stffabi"/><br /><sub><b>stffabi</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=stffabi" title="Code">💻</a> <a href="#maintenance-stffabi" title="Maintenance">🚧</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/stffabi"><img src="https://avatars0.githubusercontent.com/u/9464631?v=4?s=100" width="100px;" alt="stffabi"/><br /><sub><b>stffabi</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=stffabi" title="Code">💻</a> <a href="#maintenance-stffabi" title="Maintenance">🚧</a></td>
<td align="center"><a href="https://github.com/ATCUSA"><img src="https://avatars3.githubusercontent.com/u/3581228?v=4?s=100" width="100px;" alt="Austin"/><br /><sub><b>Austin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ATCUSA" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ATCUSA"><img src="https://avatars3.githubusercontent.com/u/3581228?v=4?s=100" width="100px;" alt="Austin"/><br /><sub><b>Austin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ATCUSA" title="Documentation">📖</a></td>
<td align="center"><a href="https://labs.ctl.io"><img src="https://avatars2.githubusercontent.com/u/6181487?v=4?s=100" width="100px;" alt="David Gardner"/><br /><sub><b>David Gardner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Adavidgardner11" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=davidgardner11" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://labs.ctl.io"><img src="https://avatars2.githubusercontent.com/u/6181487?v=4?s=100" width="100px;" alt="David Gardner"/><br /><sub><b>David Gardner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Adavidgardner11" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=davidgardner11" title="Documentation">📖</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/dolanor"><img src="https://avatars3.githubusercontent.com/u/928722?v=4?s=100" width="100px;" alt="Tanguy ⧓ Herrmann"/><br /><sub><b>Tanguy ⧓ Herrmann</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dolanor" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/dolanor"><img src="https://avatars3.githubusercontent.com/u/928722?v=4?s=100" width="100px;" alt="Tanguy ⧓ Herrmann"/><br /><sub><b>Tanguy ⧓ Herrmann</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dolanor" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rdamazio"><img src="https://avatars3.githubusercontent.com/u/997641?v=4?s=100" width="100px;" alt="Rodrigo Damazio Bovendorp"/><br /><sub><b>Rodrigo Damazio Bovendorp</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/rdamazio"><img src="https://avatars3.githubusercontent.com/u/997641?v=4?s=100" width="100px;" alt="Rodrigo Damazio Bovendorp"/><br /><sub><b>Rodrigo Damazio Bovendorp</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=100" width="100px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="#infra-thelamer" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=100" width="100px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="#infra-thelamer" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/cnrmck"><img src="https://avatars2.githubusercontent.com/u/22061955?v=4?s=100" width="100px;" alt="cnrmck"/><br /><sub><b>cnrmck</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cnrmck" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/cnrmck"><img src="https://avatars2.githubusercontent.com/u/22061955?v=4?s=100" width="100px;" alt="cnrmck"/><br /><sub><b>cnrmck</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cnrmck" title="Documentation">📖</a></td>
<td align="center"><a href="http://harrywalter.co.uk"><img src="https://avatars3.githubusercontent.com/u/338588?v=4?s=100" width="100px;" alt="Harry Walter"/><br /><sub><b>Harry Walter</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=haswalt" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://harrywalter.co.uk"><img src="https://avatars3.githubusercontent.com/u/338588?v=4?s=100" width="100px;" alt="Harry Walter"/><br /><sub><b>Harry Walter</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=haswalt" title="Code">💻</a></td>
<td align="center"><a href="http://projectsperanza.com"><img src="https://avatars3.githubusercontent.com/u/74515?v=4?s=100" width="100px;" alt="Robotex"/><br /><sub><b>Robotex</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Robotex" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://projectsperanza.com"><img src="https://avatars3.githubusercontent.com/u/74515?v=4?s=100" width="100px;" alt="Robotex"/><br /><sub><b>Robotex</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Robotex" title="Documentation">📖</a></td>
<td align="center"><a href="http://geraldpape.io"><img src="https://avatars0.githubusercontent.com/u/1494211?v=4?s=100" width="100px;" alt="Gerald Pape"/><br /><sub><b>Gerald Pape</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ubergesundheit" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://geraldpape.io"><img src="https://avatars0.githubusercontent.com/u/1494211?v=4?s=100" width="100px;" alt="Gerald Pape"/><br /><sub><b>Gerald Pape</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ubergesundheit" title="Documentation">📖</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/fomk"><img src="https://avatars0.githubusercontent.com/u/17636183?v=4?s=100" width="100px;" alt="fomk"/><br /><sub><b>fomk</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=fomk" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/fomk"><img src="https://avatars0.githubusercontent.com/u/17636183?v=4?s=100" width="100px;" alt="fomk"/><br /><sub><b>fomk</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=fomk" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/svengo"><img src="https://avatars3.githubusercontent.com/u/2502366?v=4?s=100" width="100px;" alt="Sven Gottwald"/><br /><sub><b>Sven Gottwald</b></sub></a><br /><a href="#infra-svengo" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/svengo"><img src="https://avatars3.githubusercontent.com/u/2502366?v=4?s=100" width="100px;" alt="Sven Gottwald"/><br /><sub><b>Sven Gottwald</b></sub></a><br /><a href="#infra-svengo" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://liberapay.com/techknowlogick/"><img src="https://avatars1.githubusercontent.com/u/164197?v=4?s=100" width="100px;" alt="techknowlogick"/><br /><sub><b>techknowlogick</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=techknowlogick" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://liberapay.com/techknowlogick/"><img src="https://avatars1.githubusercontent.com/u/164197?v=4?s=100" width="100px;" alt="techknowlogick"/><br /><sub><b>techknowlogick</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=techknowlogick" title="Code">💻</a></td>
<td align="center"><a href="http://log.c5t.org/about/"><img src="https://avatars1.githubusercontent.com/u/1449568?v=4?s=100" width="100px;" alt="waja"/><br /><sub><b>waja</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=waja" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://log.c5t.org/about/"><img src="https://avatars1.githubusercontent.com/u/1449568?v=4?s=100" width="100px;" alt="waja"/><br /><sub><b>waja</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=waja" title="Documentation">📖</a></td>
<td align="center"><a href="http://scottalbertson.com"><img src="https://avatars2.githubusercontent.com/u/154463?v=4?s=100" width="100px;" alt="Scott Albertson"/><br /><sub><b>Scott Albertson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=salbertson" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://scottalbertson.com"><img src="https://avatars2.githubusercontent.com/u/154463?v=4?s=100" width="100px;" alt="Scott Albertson"/><br /><sub><b>Scott Albertson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=salbertson" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/huddlesj"><img src="https://avatars1.githubusercontent.com/u/11966535?v=4?s=100" width="100px;" alt="Jason Huddleston"/><br /><sub><b>Jason Huddleston</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=huddlesj" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/huddlesj"><img src="https://avatars1.githubusercontent.com/u/11966535?v=4?s=100" width="100px;" alt="Jason Huddleston"/><br /><sub><b>Jason Huddleston</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=huddlesj" title="Documentation">📖</a></td>
<td align="center"><a href="https://npstr.space/"><img src="https://avatars3.githubusercontent.com/u/6048348?v=4?s=100" width="100px;" alt="Napster"/><br /><sub><b>Napster</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=napstr" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://npstr.space/"><img src="https://avatars3.githubusercontent.com/u/6048348?v=4?s=100" width="100px;" alt="Napster"/><br /><sub><b>Napster</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=napstr" title="Code">💻</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/darknode"><img src="https://avatars1.githubusercontent.com/u/809429?v=4?s=100" width="100px;" alt="Maxim"/><br /><sub><b>Maxim</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/darknode"><img src="https://avatars1.githubusercontent.com/u/809429?v=4?s=100" width="100px;" alt="Maxim"/><br /><sub><b>Maxim</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Documentation">📖</a></td>
<td align="center"><a href="https://schmitt.cat"><img src="https://avatars0.githubusercontent.com/u/17984549?v=4?s=100" width="100px;" alt="Max Schmitt"/><br /><sub><b>Max Schmitt</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mxschmitt" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://schmitt.cat"><img src="https://avatars0.githubusercontent.com/u/17984549?v=4?s=100" width="100px;" alt="Max Schmitt"/><br /><sub><b>Max Schmitt</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mxschmitt" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/cron410"><img src="https://avatars1.githubusercontent.com/u/3082899?v=4?s=100" width="100px;" alt="cron410"/><br /><sub><b>cron410</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cron410" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/cron410"><img src="https://avatars1.githubusercontent.com/u/3082899?v=4?s=100" width="100px;" alt="cron410"/><br /><sub><b>cron410</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cron410" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Cardoso222"><img src="https://avatars3.githubusercontent.com/u/7026517?v=4?s=100" width="100px;" alt="Paulo Henrique"/><br /><sub><b>Paulo Henrique</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Cardoso222" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Cardoso222"><img src="https://avatars3.githubusercontent.com/u/7026517?v=4?s=100" width="100px;" alt="Paulo Henrique"/><br /><sub><b>Paulo Henrique</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Cardoso222" title="Documentation">📖</a></td>
<td align="center"><a href="https://coded.io"><img src="https://avatars0.githubusercontent.com/u/107097?v=4?s=100" width="100px;" alt="Kaleb Elwert"/><br /><sub><b>Kaleb Elwert</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=belak" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://coded.io"><img src="https://avatars0.githubusercontent.com/u/107097?v=4?s=100" width="100px;" alt="Kaleb Elwert"/><br /><sub><b>Kaleb Elwert</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=belak" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/wmbutler"><img src="https://avatars1.githubusercontent.com/u/1254810?v=4?s=100" width="100px;" alt="Bill Butler"/><br /><sub><b>Bill Butler</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=wmbutler" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/wmbutler"><img src="https://avatars1.githubusercontent.com/u/1254810?v=4?s=100" width="100px;" alt="Bill Butler"/><br /><sub><b>Bill Butler</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=wmbutler" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.mariotacke.io"><img src="https://avatars2.githubusercontent.com/u/4942019?v=4?s=100" width="100px;" alt="Mario Tacke"/><br /><sub><b>Mario Tacke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mariotacke" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.mariotacke.io"><img src="https://avatars2.githubusercontent.com/u/4942019?v=4?s=100" width="100px;" alt="Mario Tacke"/><br /><sub><b>Mario Tacke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mariotacke" title="Code">💻</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4?s=100" width="100px;" alt="Mark Woodbridge"/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4?s=100" width="100px;" alt="Mark Woodbridge"/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td>
<td align="center"><a href="http://simme.dev"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4?s=100" width="100px;" alt="Simon Aronsson"/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://simme.dev"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4?s=100" width="100px;" alt="Simon Aronsson"/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4?s=100" width="100px;" alt="Ansem93"/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4?s=100" width="100px;" alt="Ansem93"/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4?s=100" width="100px;" alt="Luka Peschke"/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4?s=100" width="100px;" alt="Luka Peschke"/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4?s=100" width="100px;" alt="Zois Pagoulatos"/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-zoispag" title="Maintenance">🚧</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4?s=100" width="100px;" alt="Zois Pagoulatos"/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-zoispag" title="Maintenance">🚧</a></td>
<td align="center"><a href="https://alexandre.menif.name"><img src="https://avatars0.githubusercontent.com/u/16152103?v=4?s=100" width="100px;" alt="Alexandre Menif"/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=alexandremenif" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://alexandre.menif.name"><img src="https://avatars0.githubusercontent.com/u/16152103?v=4?s=100" width="100px;" alt="Alexandre Menif"/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=alexandremenif" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/chugunov"><img src="https://avatars1.githubusercontent.com/u/4140479?v=4?s=100" width="100px;" alt="Andrey"/><br /><sub><b>Andrey</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chugunov" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/chugunov"><img src="https://avatars1.githubusercontent.com/u/4140479?v=4?s=100" width="100px;" alt="Andrey"/><br /><sub><b>Andrey</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chugunov" title="Documentation">📖</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://noplanman.ch"><img src="https://avatars3.githubusercontent.com/u/9423417?v=4?s=100" width="100px;" alt="Armando Lüscher"/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=noplanman" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://noplanman.ch"><img src="https://avatars3.githubusercontent.com/u/9423417?v=4?s=100" width="100px;" alt="Armando Lüscher"/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=noplanman" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/rjbudke"><img src="https://avatars2.githubusercontent.com/u/273485?v=4?s=100" width="100px;" alt="Ryan Budke"/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rjbudke" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/rjbudke"><img src="https://avatars2.githubusercontent.com/u/273485?v=4?s=100" width="100px;" alt="Ryan Budke"/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rjbudke" title="Documentation">📖</a></td>
<td align="center"><a href="http://kaloyan.raev.name"><img src="https://avatars2.githubusercontent.com/u/468091?v=4?s=100" width="100px;" alt="Kaloyan Raev"/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="http://kaloyan.raev.name"><img src="https://avatars2.githubusercontent.com/u/468091?v=4?s=100" width="100px;" alt="Kaloyan Raev"/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/sixth"><img src="https://avatars3.githubusercontent.com/u/11591445?v=4?s=100" width="100px;" alt="sixth"/><br /><sub><b>sixth</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixth" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sixth"><img src="https://avatars3.githubusercontent.com/u/11591445?v=4?s=100" width="100px;" alt="sixth"/><br /><sub><b>sixth</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixth" title="Documentation">📖</a></td>
<td align="center"><a href="https://foosel.net"><img src="https://avatars0.githubusercontent.com/u/83657?v=4?s=100" width="100px;" alt="Gina Häußge"/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=foosel" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://foosel.net"><img src="https://avatars0.githubusercontent.com/u/83657?v=4?s=100" width="100px;" alt="Gina Häußge"/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=foosel" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/8ear"><img src="https://avatars0.githubusercontent.com/u/10329648?v=4?s=100" width="100px;" alt="Max H."/><br /><sub><b>Max H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=8ear" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/8ear"><img src="https://avatars0.githubusercontent.com/u/10329648?v=4?s=100" width="100px;" alt="Max H."/><br /><sub><b>Max H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=8ear" title="Code">💻</a></td>
<td align="center"><a href="https://pjknkda.github.io"><img src="https://avatars0.githubusercontent.com/u/4986524?v=4?s=100" width="100px;" alt="Jungkook Park"/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pjknkda" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://pjknkda.github.io"><img src="https://avatars0.githubusercontent.com/u/4986524?v=4?s=100" width="100px;" alt="Jungkook Park"/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pjknkda" title="Documentation">📖</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://achfrag.net"><img src="https://avatars1.githubusercontent.com/u/5753622?v=4?s=100" width="100px;" alt="Jan Kristof Nidzwetzki"/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jnidzwetzki" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://achfrag.net"><img src="https://avatars1.githubusercontent.com/u/5753622?v=4?s=100" width="100px;" alt="Jan Kristof Nidzwetzki"/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jnidzwetzki" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.lukaselsner.de"><img src="https://avatars0.githubusercontent.com/u/1413542?v=4?s=100" width="100px;" alt="lukas"/><br /><sub><b>lukas</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mindrunner" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.lukaselsner.de"><img src="https://avatars0.githubusercontent.com/u/1413542?v=4?s=100" width="100px;" alt="lukas"/><br /><sub><b>lukas</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mindrunner" title="Code">💻</a></td>
<td align="center"><a href="https://codingcoffee.dev"><img src="https://avatars3.githubusercontent.com/u/13611153?v=4?s=100" width="100px;" alt="Ameya Shenoy"/><br /><sub><b>Ameya Shenoy</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=codingCoffee" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://codingcoffee.dev"><img src="https://avatars3.githubusercontent.com/u/13611153?v=4?s=100" width="100px;" alt="Ameya Shenoy"/><br /><sub><b>Ameya Shenoy</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=codingCoffee" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/raymondelooff"><img src="https://avatars0.githubusercontent.com/u/9716806?v=4?s=100" width="100px;" alt="Raymon de Looff"/><br /><sub><b>Raymon de Looff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=raymondelooff" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/raymondelooff"><img src="https://avatars0.githubusercontent.com/u/9716806?v=4?s=100" width="100px;" alt="Raymon de Looff"/><br /><sub><b>Raymon de Looff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=raymondelooff" title="Code">💻</a></td>
<td align="center"><a href="http://codemonkeylabs.com"><img src="https://avatars2.githubusercontent.com/u/704034?v=4?s=100" width="100px;" alt="John Clayton"/><br /><sub><b>John Clayton</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jsclayton" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://codemonkeylabs.com"><img src="https://avatars2.githubusercontent.com/u/704034?v=4?s=100" width="100px;" alt="John Clayton"/><br /><sub><b>John Clayton</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jsclayton" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Germs2004"><img src="https://avatars2.githubusercontent.com/u/5519340?v=4?s=100" width="100px;" alt="Germs2004"/><br /><sub><b>Germs2004</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Germs2004" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Germs2004"><img src="https://avatars2.githubusercontent.com/u/5519340?v=4?s=100" width="100px;" alt="Germs2004"/><br /><sub><b>Germs2004</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Germs2004" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/lukwil"><img src="https://avatars1.githubusercontent.com/u/30203234?v=4?s=100" width="100px;" alt="Lukas Willburger"/><br /><sub><b>Lukas Willburger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukwil" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/lukwil"><img src="https://avatars1.githubusercontent.com/u/30203234?v=4?s=100" width="100px;" alt="Lukas Willburger"/><br /><sub><b>Lukas Willburger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukwil" title="Code">💻</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/auanasgheps"><img src="https://avatars2.githubusercontent.com/u/20586878?v=4?s=100" width="100px;" alt="Oliver Cervera"/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=auanasgheps" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/auanasgheps"><img src="https://avatars2.githubusercontent.com/u/20586878?v=4?s=100" width="100px;" alt="Oliver Cervera"/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=auanasgheps" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/victorcmoura"><img src="https://avatars1.githubusercontent.com/u/26290053?v=4?s=100" width="100px;" alt="Victor Moura"/><br /><sub><b>Victor Moura</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/victorcmoura"><img src="https://avatars1.githubusercontent.com/u/26290053?v=4?s=100" width="100px;" alt="Victor Moura"/><br /><sub><b>Victor Moura</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4?s=100" width="100px;" alt="Maximilian Brandau"/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4?s=100" width="100px;" alt="Maximilian Brandau"/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4?s=100" width="100px;" alt="Andrew"/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4?s=100" width="100px;" alt="Andrew"/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4?s=100" width="100px;" alt="sixcorners"/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4?s=100" width="100px;" alt="sixcorners"/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td>
<td align="center"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4?s=100" width="100px;" alt="nils måsén"/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4?s=100" width="100px;" alt="nils måsén"/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Code">💻</a></td>
<td align="center"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4?s=100" width="100px;" alt="Arne Jørgensen"/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td> <td align="center" valign="top" width="14.28%"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4?s=100" width="100px;" alt="Arne Jørgensen"/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/patski123"><img src="https://avatars1.githubusercontent.com/u/19295295?v=4?s=100" width="100px;" alt="PatSki123"/><br /><sub><b>PatSki123</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patski123" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/patski123"><img src="https://avatars1.githubusercontent.com/u/19295295?v=4?s=100" width="100px;" alt="PatSki123"/><br /><sub><b>PatSki123</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patski123" title="Documentation">📖</a></td>
<td align="center"><a href="https://rubyroidlabs.com/"><img src="https://avatars2.githubusercontent.com/u/624999?v=4?s=100" width="100px;" alt="Valentine Zavadsky"/><br /><sub><b>Valentine Zavadsky</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://rubyroidlabs.com/"><img src="https://avatars2.githubusercontent.com/u/624999?v=4?s=100" width="100px;" alt="Valentine Zavadsky"/><br /><sub><b>Valentine Zavadsky</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/bopoh24"><img src="https://avatars2.githubusercontent.com/u/4086631?v=4?s=100" width="100px;" alt="Alexander Voronin"/><br /><sub><b>Alexander Voronin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bopoh24" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/issues?q=author%3Abopoh24" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/bopoh24"><img src="https://avatars2.githubusercontent.com/u/4086631?v=4?s=100" width="100px;" alt="Alexander Voronin"/><br /><sub><b>Alexander Voronin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bopoh24" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/issues?q=author%3Abopoh24" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://www.teqneers.de"><img src="https://avatars0.githubusercontent.com/u/788989?v=4?s=100" width="100px;" alt="Oliver Mueller"/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ogmueller" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.teqneers.de"><img src="https://avatars0.githubusercontent.com/u/788989?v=4?s=100" width="100px;" alt="Oliver Mueller"/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ogmueller" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/tammert"><img src="https://avatars0.githubusercontent.com/u/8885250?v=4?s=100" width="100px;" alt="Sebastiaan Tammer"/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tammert" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/tammert"><img src="https://avatars0.githubusercontent.com/u/8885250?v=4?s=100" width="100px;" alt="Sebastiaan Tammer"/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tammert" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Miosame"><img src="https://avatars1.githubusercontent.com/u/8201077?v=4?s=100" width="100px;" alt="miosame"/><br /><sub><b>miosame</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=miosame" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Miosame"><img src="https://avatars1.githubusercontent.com/u/8201077?v=4?s=100" width="100px;" alt="miosame"/><br /><sub><b>miosame</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=miosame" title="Documentation">📖</a></td>
<td align="center"><a href="https://mtz.gr"><img src="https://avatars3.githubusercontent.com/u/590246?v=4?s=100" width="100px;" alt="Andrew Metzger"/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger" title="Bug reports">🐛</a> <a href="#example-andrewjmetzger" title="Examples">💡</a></td> <td align="center" valign="top" width="14.28%"><a href="https://mtz.gr"><img src="https://avatars3.githubusercontent.com/u/590246?v=4?s=100" width="100px;" alt="Andrew Metzger"/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger" title="Bug reports">🐛</a> <a href="#example-andrewjmetzger" title="Examples">💡</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4?s=100" width="100px;" alt="Pierre Grimaud"/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pgrimaud" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4?s=100" width="100px;" alt="Pierre Grimaud"/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pgrimaud" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mattdoran"><img src="https://avatars0.githubusercontent.com/u/577779?v=4?s=100" width="100px;" alt="Matt Doran"/><br /><sub><b>Matt Doran</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mattdoran" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/mattdoran"><img src="https://avatars0.githubusercontent.com/u/577779?v=4?s=100" width="100px;" alt="Matt Doran"/><br /><sub><b>Matt Doran</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mattdoran" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/MihailITPlace"><img src="https://avatars2.githubusercontent.com/u/28401551?v=4?s=100" width="100px;" alt="MihailITPlace"/><br /><sub><b>MihailITPlace</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MihailITPlace" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/MihailITPlace"><img src="https://avatars2.githubusercontent.com/u/28401551?v=4?s=100" width="100px;" alt="MihailITPlace"/><br /><sub><b>MihailITPlace</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MihailITPlace" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bugficks"><img src="https://avatars1.githubusercontent.com/u/2992895?v=4?s=100" width="100px;" alt="bugficks"/><br /><sub><b>bugficks</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/bugficks"><img src="https://avatars1.githubusercontent.com/u/2992895?v=4?s=100" width="100px;" alt="bugficks"/><br /><sub><b>bugficks</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/MichaelSp"><img src="https://avatars0.githubusercontent.com/u/448282?v=4?s=100" width="100px;" alt="Michael"/><br /><sub><b>Michael</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MichaelSp" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/MichaelSp"><img src="https://avatars0.githubusercontent.com/u/448282?v=4?s=100" width="100px;" alt="Michael"/><br /><sub><b>Michael</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MichaelSp" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jokay"><img src="https://avatars0.githubusercontent.com/u/18613935?v=4?s=100" width="100px;" alt="D. Domig"/><br /><sub><b>D. Domig</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jokay" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jokay"><img src="https://avatars0.githubusercontent.com/u/18613935?v=4?s=100" width="100px;" alt="D. Domig"/><br /><sub><b>D. Domig</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jokay" title="Documentation">📖</a></td>
<td align="center"><a href="https://maxwells-daemon.io"><img src="https://avatars1.githubusercontent.com/u/260084?v=4?s=100" width="100px;" alt="Ben Osheroff"/><br /><sub><b>Ben Osheroff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=osheroff" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://maxwells-daemon.io"><img src="https://avatars1.githubusercontent.com/u/260084?v=4?s=100" width="100px;" alt="Ben Osheroff"/><br /><sub><b>Ben Osheroff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=osheroff" title="Code">💻</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/dhet"><img src="https://avatars3.githubusercontent.com/u/2668621?v=4?s=100" width="100px;" alt="David H."/><br /><sub><b>David H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dhet" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/dhet"><img src="https://avatars3.githubusercontent.com/u/2668621?v=4?s=100" width="100px;" alt="David H."/><br /><sub><b>David H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dhet" title="Code">💻</a></td>
<td align="center"><a href="http://www.gridgeo.com"><img src="https://avatars1.githubusercontent.com/u/671887?v=4?s=100" width="100px;" alt="Chander Ganesan"/><br /><sub><b>Chander Ganesan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chander" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.gridgeo.com"><img src="https://avatars1.githubusercontent.com/u/671887?v=4?s=100" width="100px;" alt="Chander Ganesan"/><br /><sub><b>Chander Ganesan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chander" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/yrien30"><img src="https://avatars1.githubusercontent.com/u/26816162?v=4?s=100" width="100px;" alt="yrien30"/><br /><sub><b>yrien30</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=yrien30" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/yrien30"><img src="https://avatars1.githubusercontent.com/u/26816162?v=4?s=100" width="100px;" alt="yrien30"/><br /><sub><b>yrien30</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=yrien30" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ksurl"><img src="https://avatars1.githubusercontent.com/u/1371562?v=4?s=100" width="100px;" alt="ksurl"/><br /><sub><b>ksurl</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Code">💻</a> <a href="#infra-ksurl" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ksurl"><img src="https://avatars1.githubusercontent.com/u/1371562?v=4?s=100" width="100px;" alt="ksurl"/><br /><sub><b>ksurl</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Code">💻</a> <a href="#infra-ksurl" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/rg9400"><img src="https://avatars2.githubusercontent.com/u/39887349?v=4?s=100" width="100px;" alt="rg9400"/><br /><sub><b>rg9400</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rg9400" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/rg9400"><img src="https://avatars2.githubusercontent.com/u/39887349?v=4?s=100" width="100px;" alt="rg9400"/><br /><sub><b>rg9400</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rg9400" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tkalus"><img src="https://avatars2.githubusercontent.com/u/287181?v=4?s=100" width="100px;" alt="Turtle Kalus"/><br /><sub><b>Turtle Kalus</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tkalus" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/tkalus"><img src="https://avatars2.githubusercontent.com/u/287181?v=4?s=100" width="100px;" alt="Turtle Kalus"/><br /><sub><b>Turtle Kalus</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tkalus" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SrihariThalla"><img src="https://avatars1.githubusercontent.com/u/7479937?v=4?s=100" width="100px;" alt="Srihari Thalla"/><br /><sub><b>Srihari Thalla</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=SrihariThalla" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/SrihariThalla"><img src="https://avatars1.githubusercontent.com/u/7479937?v=4?s=100" width="100px;" alt="Srihari Thalla"/><br /><sub><b>Srihari Thalla</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=SrihariThalla" title="Documentation">📖</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://nymous.io"><img src="https://avatars1.githubusercontent.com/u/4216559?v=4?s=100" width="100px;" alt="Thomas Gaudin"/><br /><sub><b>Thomas Gaudin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nymous" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://nymous.io"><img src="https://avatars1.githubusercontent.com/u/4216559?v=4?s=100" width="100px;" alt="Thomas Gaudin"/><br /><sub><b>Thomas Gaudin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nymous" title="Documentation">📖</a></td>
<td align="center"><a href="https://indigo.re/"><img src="https://avatars.githubusercontent.com/u/2804645?v=4?s=100" width="100px;" alt="hydrargyrum"/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hydrargyrum" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://indigo.re/"><img src="https://avatars.githubusercontent.com/u/2804645?v=4?s=100" width="100px;" alt="hydrargyrum"/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hydrargyrum" title="Documentation">📖</a></td>
<td align="center"><a href="https://reinout.vanrees.org"><img src="https://avatars.githubusercontent.com/u/121433?v=4?s=100" width="100px;" alt="Reinout van Rees"/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=reinout" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://reinout.vanrees.org"><img src="https://avatars.githubusercontent.com/u/121433?v=4?s=100" width="100px;" alt="Reinout van Rees"/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=reinout" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/DasSkelett"><img src="https://avatars.githubusercontent.com/u/28812678?v=4?s=100" width="100px;" alt="DasSkelett"/><br /><sub><b>DasSkelett</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=DasSkelett" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/DasSkelett"><img src="https://avatars.githubusercontent.com/u/28812678?v=4?s=100" width="100px;" alt="DasSkelett"/><br /><sub><b>DasSkelett</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=DasSkelett" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/zenjabba"><img src="https://avatars.githubusercontent.com/u/679864?v=4?s=100" width="100px;" alt="zenjabba"/><br /><sub><b>zenjabba</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zenjabba" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/zenjabba"><img src="https://avatars.githubusercontent.com/u/679864?v=4?s=100" width="100px;" alt="zenjabba"/><br /><sub><b>zenjabba</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zenjabba" title="Documentation">📖</a></td>
<td align="center"><a href="https://quan.io"><img src="https://avatars.githubusercontent.com/u/3526705?v=4?s=100" width="100px;" alt="Dan Quan"/><br /><sub><b>Dan Quan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=djquan" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://quan.io"><img src="https://avatars.githubusercontent.com/u/3526705?v=4?s=100" width="100px;" alt="Dan Quan"/><br /><sub><b>Dan Quan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=djquan" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/modem7"><img src="https://avatars.githubusercontent.com/u/4349962?v=4?s=100" width="100px;" alt="modem7"/><br /><sub><b>modem7</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=modem7" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/modem7"><img src="https://avatars.githubusercontent.com/u/4349962?v=4?s=100" width="100px;" alt="modem7"/><br /><sub><b>modem7</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=modem7" title="Documentation">📖</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/hypnoglow"><img src="https://avatars.githubusercontent.com/u/4853075?v=4?s=100" width="100px;" alt="Igor Zibarev"/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hypnoglow" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/hypnoglow"><img src="https://avatars.githubusercontent.com/u/4853075?v=4?s=100" width="100px;" alt="Igor Zibarev"/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hypnoglow" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/patricegautier"><img src="https://avatars.githubusercontent.com/u/38435239?v=4?s=100" width="100px;" alt="Patrice"/><br /><sub><b>Patrice</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patricegautier" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/patricegautier"><img src="https://avatars.githubusercontent.com/u/38435239?v=4?s=100" width="100px;" alt="Patrice"/><br /><sub><b>Patrice</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patricegautier" title="Code">💻</a></td>
<td align="center"><a href="http://jamesw.link/me"><img src="https://avatars.githubusercontent.com/u/8067792?v=4?s=100" width="100px;" alt="James White"/><br /><sub><b>James White</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jamesmacwhite" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://jamesw.link/me"><img src="https://avatars.githubusercontent.com/u/8067792?v=4?s=100" width="100px;" alt="James White"/><br /><sub><b>James White</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jamesmacwhite" title="Documentation">📖</a></td>
<td align="center"><a href="https://ko-fi.com/foxite"><img src="https://avatars.githubusercontent.com/u/20421657?v=4?s=100" width="100px;" alt="Dirk Kok"/><br /><sub><b>Dirk Kok</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Foxite" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://ko-fi.com/foxite"><img src="https://avatars.githubusercontent.com/u/20421657?v=4?s=100" width="100px;" alt="Dirk Kok"/><br /><sub><b>Dirk Kok</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Foxite" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/EDIflyer"><img src="https://avatars.githubusercontent.com/u/13610277?v=4?s=100" width="100px;" alt="EDIflyer"/><br /><sub><b>EDIflyer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=EDIflyer" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/EDIflyer"><img src="https://avatars.githubusercontent.com/u/13610277?v=4?s=100" width="100px;" alt="EDIflyer"/><br /><sub><b>EDIflyer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=EDIflyer" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jauderho"><img src="https://avatars.githubusercontent.com/u/13562?v=4?s=100" width="100px;" alt="Jauder Ho"/><br /><sub><b>Jauder Ho</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jauderho" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jauderho"><img src="https://avatars.githubusercontent.com/u/13562?v=4?s=100" width="100px;" alt="Jauder Ho"/><br /><sub><b>Jauder Ho</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jauderho" title="Code">💻</a></td>
<td align="center"><a href="https://tamal.vercel.app/"><img src="https://avatars.githubusercontent.com/u/72851613?v=4?s=100" width="100px;" alt="Tamal Das "/><br /><sub><b>Tamal Das </b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=IAmTamal" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://tamal.vercel.app/"><img src="https://avatars.githubusercontent.com/u/72851613?v=4?s=100" width="100px;" alt="Tamal Das "/><br /><sub><b>Tamal Das </b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=IAmTamal" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/testwill"><img src="https://avatars.githubusercontent.com/u/8717479?v=4?s=100" width="100px;" alt="guangwu"/><br /><sub><b>guangwu</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=testwill" title="Documentation">📖</a></td>
</tr> </tr>
</tbody> </tbody>
<tfoot>
</tfoot>
</table> </table>
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

View file

@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"errors"
"math" "math"
"net/http" "net/http"
"os" "os"
@ -39,6 +40,7 @@ var (
lifecycleHooks bool lifecycleHooks bool
rollingRestart bool rollingRestart bool
scope string scope string
labelPrecedence bool
) )
var rootCmd = NewRootCommand() var rootCmd = NewRootCommand()
@ -77,23 +79,8 @@ func Execute() {
func PreRun(cmd *cobra.Command, _ []string) { func PreRun(cmd *cobra.Command, _ []string) {
f := cmd.PersistentFlags() f := cmd.PersistentFlags()
flags.ProcessFlagAliases(f) flags.ProcessFlagAliases(f)
if err := flags.SetupLogging(f); err != nil {
if enabled, _ := f.GetBool("no-color"); enabled { log.Fatalf("Failed to initialize logging: %s", err.Error())
log.SetFormatter(&log.TextFormatter{
DisableColors: true,
})
} else {
// enable logrus built-in support for https://bixense.com/clicolors/
log.SetFormatter(&log.TextFormatter{
EnvironmentOverrideColors: true,
})
}
rawLogLevel, _ := f.GetString(`log-level`)
if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
log.Fatalf("Invalid log level: %s", err.Error())
} else {
log.SetLevel(logLevel)
} }
scheduleSpec, _ = f.GetString("schedule") scheduleSpec, _ = f.GetString("schedule")
@ -109,6 +96,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
rollingRestart, _ = f.GetBool("rolling-restart") rollingRestart, _ = f.GetBool("rolling-restart")
scope, _ = f.GetString("scope") scope, _ = f.GetString("scope")
labelPrecedence, _ = f.GetBool("label-take-precedence")
if scope != "" { if scope != "" {
log.Debugf(`Using scope %q`, scope) log.Debugf(`Using scope %q`, scope)
@ -209,7 +197,7 @@ func Run(c *cobra.Command, names []string) {
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
} }
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed { if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error("failed to start API", err) log.Error("failed to start API", err)
} }
@ -376,6 +364,7 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
MonitorOnly: monitorOnly, MonitorOnly: monitorOnly,
LifecycleHooks: lifecycleHooks, LifecycleHooks: lifecycleHooks,
RollingRestart: rollingRestart, RollingRestart: rollingRestart,
LabelPrecedence: labelPrecedence,
} }
result, err := actions.Update(client, updateParams) result, err := actions.Update(client, updateParams)
if err != nil { if err != nil {

View file

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM alpine:3.18.2 as alpine FROM --platform=$BUILDPLATFORM alpine:3.18.3 as alpine
RUN apk add --no-cache \ RUN apk add --no-cache \
ca-certificates \ ca-certificates \

View file

@ -27,6 +27,33 @@ In the example above, watchtower will execute an upgrade attempt on the containe
When no arguments are specified, watchtower will monitor all running containers. When no arguments are specified, watchtower will monitor all running containers.
## Secrets/Files
Some arguments can also reference a file, in which case the contents of the file are used as the value.
This can be used to avoid putting secrets in the configuration file or command line.
The following arguments are currently supported (including their corresponding `WATCHTOWER_` environment variables):
- `notification-url`
- `notification-email-server-password`
- `notification-slack-hook-url`
- `notification-msteams-hook`
- `notification-gotify-token`
- `http-api-token`
### Example docker-compose usage
```yaml
secrets:
access_token:
file: access_token
services:
watchtower:
secrets:
- access_token
environment:
- WATCHTOWER_HTTP_API_TOKEN=/run/secrets/access_token
```
## Help ## Help
Shows documentation about the supported flags. Shows documentation about the supported flags.
@ -107,6 +134,17 @@ Environment Variable: WATCHTOWER_LOG_LEVEL
Default: info Default: info
``` ```
## Logging format
Sets what logging format to use for console output.
```text
Argument: --log-format, -l
Environment Variable: WATCHTOWER_LOG_FORMAT
Possible values: Auto, LogFmt, Pretty or JSON
Default: Auto
```
## ANSI colors ## ANSI colors
Disable ANSI color escape codes in log output. Disable ANSI color escape codes in log output.
@ -151,7 +189,7 @@ Environment Variable: WATCHTOWER_INCLUDE_RESTARTING
Will also include created and exited containers. Will also include created and exited containers.
```text ```text
Argument: --include-stopped Argument: --include-stopped, -S
Environment Variable: WATCHTOWER_INCLUDE_STOPPED Environment Variable: WATCHTOWER_INCLUDE_STOPPED
Type: Boolean Type: Boolean
Default: false Default: false
@ -178,7 +216,7 @@ Environment Variable: WATCHTOWER_POLL_INTERVAL
``` ```
## Filter by enable label ## Filter by enable label
Update containers that have a `com.centurylinklabs.watchtower.enable` label set to true. Monitor and update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
```text ```text
Argument: --label-enable Argument: --label-enable
@ -188,7 +226,7 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE
``` ```
## Filter by disable label ## Filter by disable label
__Do not__ update containers that have `com.centurylinklabs.watchtower.enable` label set to false and __Do not__ Monitor and update containers that have `com.centurylinklabs.watchtower.enable` label set to false and
no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be
used at the same time to target containers. used at the same time to target containers.
@ -211,6 +249,19 @@ Environment Variable: WATCHTOWER_MONITOR_ONLY
Note that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers. Note that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers.
See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
## With label taking precedence over arguments
By default, arguments will take precedence over labels. This means that if you set `WATCHTOWER_MONITOR_ONLY` to true or use `--monitor-only`, a container with `com.centurylinklabs.watchtower.monitor-only` set to false will not be updated. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will also be updated. This also apply to the no pull option. if you set `WATCHTOWER_NO_PULL` to true or use `--no-pull`, a container with `com.centurylinklabs.watchtower.no-pull` set to false will not pull the new image. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will pull image
```text
Argument: --label-take-precedence
Environment Variable: WATCHTOWER_LABEL_TAKE_PRECEDENCE
Type: Boolean
Default: false
```
## Without restarting containers ## Without restarting containers
Do not restart containers after updating. This option can be useful when the start of the containers Do not restart containers after updating. This option can be useful when the start of the containers
is managed by an external system such as systemd. is managed by an external system such as systemd.
@ -237,6 +288,8 @@ Environment Variable: WATCHTOWER_NO_PULL
Note that no-pull can also be specified on a per-container basis with the Note that no-pull can also be specified on a per-container basis with the
`com.centurylinklabs.watchtower.no-pull` label set on those containers. `com.centurylinklabs.watchtower.no-pull` label set on those containers.
See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
## Without sending a startup message ## Without sending a startup message
Do not send a message after watchtower started. Otherwise there will be an info-level notification. Do not send a message after watchtower started. Otherwise there will be an info-level notification.
@ -251,7 +304,7 @@ Environment Variable: WATCHTOWER_NO_STARTUP_MESSAGE
Run an update attempt against a container name list one time immediately and exit. Run an update attempt against a container name list one time immediately and exit.
```text ```text
Argument: --run-once Argument: --run-once, -R
Environment Variable: WATCHTOWER_RUN_ONCE Environment Variable: WATCHTOWER_RUN_ONCE
Type: Boolean Type: Boolean
Default: false Default: false
@ -270,6 +323,7 @@ Environment Variable: WATCHTOWER_HTTP_API_UPDATE
## HTTP API Token ## HTTP API Token
Sets an authentication token to HTTP API requests. Sets an authentication token to HTTP API requests.
Can also reference a file, in which case the contents of the file are used.
```text ```text
Argument: --http-api-token Argument: --http-api-token
@ -375,3 +429,21 @@ Returns a success exit code to enable usage with docker `HEALTHCHECK`. This chec
```text ```text
Argument: --health-check Argument: --health-check
``` ```
## Programatic Output (porcelain)
Writes the session results to STDOUT using a stable, machine-readable format (indicated by the argument VERSION).
Alias for:
```text
--notification-url logger://
--notification-log-stdout
--notification-report
--notification-template porcelain.VERSION.summary-no-log
Argument: --porcelain, -P
Environment Variable: WATCHTOWER_PORCELAIN
Possible values: v1
Default: -
```

View file

@ -11,5 +11,5 @@ CONTAINER ID IMAGE STATUS PORTS
6cc4d2a9d1a5 containrrr/watchtower Up 15 minutes watchtower 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). Every day 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).

View file

@ -18,10 +18,11 @@ system, [logrus](http://github.com/sirupsen/logrus).
- `--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`, `debug` or `trace`. - `--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`, `debug` or `trace`.
- `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname. - `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname.
- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATION_DELAY`): Delay before sending notifications expressed in seconds. - `--notifications-delay` (env. `WATCHTOWER_NOTIFICATIONS_DELAY`): Delay before sending notifications expressed in seconds.
- Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument. - Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument.
- `notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers. - `--notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers.
- `notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together. - `--notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together.
- `--notification-log-stdout` (env. `WATCHTOWER_NOTIFICATION_LOG_STDOUT`): Enable output from `logger://` shoutrrr service to stdout.
## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications ## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications
@ -30,7 +31,7 @@ To send notifications via shoutrrr, the following command-line options, or their
- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used. - `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used.
Go to [containrrr.dev/shoutrrr/v0.7/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to Go to [containrrr.dev/shoutrrr/v0.8/services/overview](https://containrrr.dev/shoutrrr/v0.8/services/overview) to
learn more about the different service URLs you can use. You can define multiple services by space separating the learn more about the different service URLs you can use. You can define multiple services by space separating the
URLs. (See example below) URLs. (See example below)

View file

@ -124,7 +124,7 @@ in a volume that may be mounted onto your watchtower container.
1. Create the Dockerfile (contents below): 1. Create the Dockerfile (contents below):
```Dockerfile ```Dockerfile
FROM golang:1.16 FROM golang:1.17
ENV GO111MODULE off ENV GO111MODULE off
ENV CGO_ENABLED 0 ENV CGO_ENABLED 0

16
go.mod
View file

@ -3,10 +3,10 @@ module github.com/containrrr/watchtower
go 1.18 go 1.18
require ( require (
github.com/containrrr/shoutrrr v0.7.1 github.com/containrrr/shoutrrr v0.8.0
github.com/docker/cli v24.0.5+incompatible github.com/docker/cli v24.0.6+incompatible
github.com/docker/distribution v2.8.2+incompatible github.com/docker/distribution v2.8.2+incompatible
github.com/docker/docker v24.0.5+incompatible github.com/docker/docker v24.0.6+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.27.10 github.com/onsi/gomega v1.27.10
@ -17,7 +17,7 @@ require (
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
golang.org/x/net v0.14.0 golang.org/x/net v0.15.0
) )
require ( require (
@ -28,7 +28,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker-credential-helpers v0.6.1 // indirect github.com/docker/docker-credential-helpers v0.6.1 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/fatih/color v1.13.0 // indirect github.com/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
@ -37,7 +37,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
@ -56,8 +56,8 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/sys v0.11.0 // indirect golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.12.0 golang.org/x/text v0.13.0
golang.org/x/time v0.1.0 // indirect golang.org/x/time v0.1.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

663
go.sum

File diff suppressed because it is too large Load diff

View file

@ -86,7 +86,7 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int)
} }
// IsContainerStale is true if not explicitly stated in TestData for the mock client // IsContainerStale is true if not explicitly stated in TestData for the mock client
func (client MockClient) IsContainerStale(cont t.Container) (bool, t.ImageID, error) { func (client MockClient) IsContainerStale(cont t.Container, params t.UpdateParams) (bool, t.ImageID, error) {
stale, found := client.TestData.Staleness[cont.Name()] stale, found := client.TestData.Staleness[cont.Name()]
if !found { if !found {
stale = true stale = true

View file

@ -33,8 +33,8 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
staleCheckFailed := 0 staleCheckFailed := 0
for i, targetContainer := range containers { for i, targetContainer := range containers {
stale, newestImage, err := client.IsContainerStale(targetContainer) stale, newestImage, err := client.IsContainerStale(targetContainer, params)
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params)
if err == nil && shouldUpdate { if err == nil && shouldUpdate {
// Check to make sure we have all the necessary information for recreating the container // Check to make sure we have all the necessary information for recreating the container
err = targetContainer.VerifyConfiguration() err = targetContainer.VerifyConfiguration()
@ -72,14 +72,12 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
UpdateImplicitRestart(containers) UpdateImplicitRestart(containers)
var containersToUpdate []types.Container var containersToUpdate []types.Container
if !params.MonitorOnly {
for _, c := range containers { for _, c := range containers {
if !c.IsMonitorOnly() { if !c.IsMonitorOnly(params) {
containersToUpdate = append(containersToUpdate, c) containersToUpdate = append(containersToUpdate, c)
progress.MarkForUpdate(c.ID()) progress.MarkForUpdate(c.ID())
} }
} }
}
if params.RollingRestart { if params.RollingRestart {
progress.UpdateFailed(performRollingRestart(containersToUpdate, client, params)) progress.UpdateFailed(performRollingRestart(containersToUpdate, client, params))

View file

@ -178,13 +178,85 @@ var _ = Describe("the update action", func() {
false, false,
false, false,
) )
_, err := actions.Update(client, types.UpdateParams{MonitorOnly: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
}) })
When("watchtower has been instructed to have label take precedence", func() {
It("it should update containers when monitor only is set to false", func() {
client := CreateMockClient(
&TestData{
//NameOfContainerToKeep: "test-container-02",
Containers: []types.Container{
CreateMockContainerWithConfig(
"test-container-02",
"test-container-02",
"fake-image2:latest",
false,
false,
time.Now(),
&dockerContainer.Config{
Labels: map[string]string{
"com.centurylinklabs.watchtower.monitor-only": "false",
},
}),
},
},
false,
false,
)
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
})
It("it should update not containers when monitor only is set to true", func() {
client := CreateMockClient(
&TestData{
//NameOfContainerToKeep: "test-container-02",
Containers: []types.Container{
CreateMockContainerWithConfig(
"test-container-02",
"test-container-02",
"fake-image2:latest",
false,
false,
time.Now(),
&dockerContainer.Config{
Labels: map[string]string{
"com.centurylinklabs.watchtower.monitor-only": "true",
},
}),
},
},
false,
false,
)
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
})
It("it should update not containers when monitor only is not set", func() {
client := CreateMockClient(
&TestData{
Containers: []types.Container{
CreateMockContainer(
"test-container-01",
"test-container-01",
"fake-image:latest",
time.Now()),
},
},
false,
false,
)
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
}) })
}) })
})
})
When("watchtower has been instructed to run lifecycle hooks", func() { When("watchtower has been instructed to run lifecycle hooks", func() {

View file

@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strings" "strings"
"time" "time"
@ -24,9 +23,9 @@ var defaultInterval = int((time.Hour * 24).Seconds())
// RegisterDockerFlags that are used directly by the docker api client // RegisterDockerFlags that are used directly by the docker api client
func RegisterDockerFlags(rootCmd *cobra.Command) { func RegisterDockerFlags(rootCmd *cobra.Command) {
flags := rootCmd.PersistentFlags() flags := rootCmd.PersistentFlags()
flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to") flags.StringP("host", "H", envString("DOCKER_HOST"), "daemon socket to connect to")
flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote") flags.BoolP("tlsverify", "v", envBool("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") flags.StringP("api-version", "a", envString("DOCKER_API_VERSION"), "api version to use by docker client")
} }
// RegisterSystemFlags that are used by watchtower to modify the program flow // RegisterSystemFlags that are used by watchtower to modify the program flow
@ -35,132 +34,138 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
flags.IntP( flags.IntP(
"interval", "interval",
"i", "i",
viper.GetInt("WATCHTOWER_POLL_INTERVAL"), envInt("WATCHTOWER_POLL_INTERVAL"),
"Poll interval (in seconds)") "Poll interval (in seconds)")
flags.StringP( flags.StringP(
"schedule", "schedule",
"s", "s",
viper.GetString("WATCHTOWER_SCHEDULE"), envString("WATCHTOWER_SCHEDULE"),
"The cron expression which defines when to update") "The cron expression which defines when to update")
flags.DurationP( flags.DurationP(
"stop-timeout", "stop-timeout",
"t", "t",
viper.GetDuration("WATCHTOWER_TIMEOUT"), envDuration("WATCHTOWER_TIMEOUT"),
"Timeout before a container is forcefully stopped") "Timeout before a container is forcefully stopped")
flags.BoolP( flags.BoolP(
"no-pull", "no-pull",
"", "",
viper.GetBool("WATCHTOWER_NO_PULL"), envBool("WATCHTOWER_NO_PULL"),
"Do not pull any new images") "Do not pull any new images")
flags.BoolP( flags.BoolP(
"no-restart", "no-restart",
"", "",
viper.GetBool("WATCHTOWER_NO_RESTART"), envBool("WATCHTOWER_NO_RESTART"),
"Do not restart any containers") "Do not restart any containers")
flags.BoolP( flags.BoolP(
"no-startup-message", "no-startup-message",
"", "",
viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), envBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
"Prevents watchtower from sending a startup message") "Prevents watchtower from sending a startup message")
flags.BoolP( flags.BoolP(
"cleanup", "cleanup",
"c", "c",
viper.GetBool("WATCHTOWER_CLEANUP"), envBool("WATCHTOWER_CLEANUP"),
"Remove previously used images after updating") "Remove previously used images after updating")
flags.BoolP( flags.BoolP(
"remove-volumes", "remove-volumes",
"", "",
viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), envBool("WATCHTOWER_REMOVE_VOLUMES"),
"Remove attached volumes before updating") "Remove attached volumes before updating")
flags.BoolP( flags.BoolP(
"label-enable", "label-enable",
"e", "e",
viper.GetBool("WATCHTOWER_LABEL_ENABLE"), envBool("WATCHTOWER_LABEL_ENABLE"),
"Watch containers where the com.centurylinklabs.watchtower.enable label is true") "Watch containers where the com.centurylinklabs.watchtower.enable label is true")
flags.StringP(
"log-format",
"l",
viper.GetString("WATCHTOWER_LOG_FORMAT"),
"Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON")
flags.BoolP( flags.BoolP(
"debug", "debug",
"d", "d",
viper.GetBool("WATCHTOWER_DEBUG"), envBool("WATCHTOWER_DEBUG"),
"Enable debug mode with verbose logging") "Enable debug mode with verbose logging")
flags.BoolP( flags.BoolP(
"trace", "trace",
"", "",
viper.GetBool("WATCHTOWER_TRACE"), envBool("WATCHTOWER_TRACE"),
"Enable trace mode with very verbose logging - caution, exposes credentials") "Enable trace mode with very verbose logging - caution, exposes credentials")
flags.BoolP( flags.BoolP(
"monitor-only", "monitor-only",
"m", "m",
viper.GetBool("WATCHTOWER_MONITOR_ONLY"), envBool("WATCHTOWER_MONITOR_ONLY"),
"Will only monitor for new images, not update the containers") "Will only monitor for new images, not update the containers")
flags.BoolP( flags.BoolP(
"run-once", "run-once",
"R", "R",
viper.GetBool("WATCHTOWER_RUN_ONCE"), envBool("WATCHTOWER_RUN_ONCE"),
"Run once now and exit") "Run once now and exit")
flags.BoolP( flags.BoolP(
"include-restarting", "include-restarting",
"", "",
viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"), envBool("WATCHTOWER_INCLUDE_RESTARTING"),
"Will also include restarting containers") "Will also include restarting containers")
flags.BoolP( flags.BoolP(
"include-stopped", "include-stopped",
"S", "S",
viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), envBool("WATCHTOWER_INCLUDE_STOPPED"),
"Will also include created and exited containers") "Will also include created and exited containers")
flags.BoolP( flags.BoolP(
"revive-stopped", "revive-stopped",
"", "",
viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), envBool("WATCHTOWER_REVIVE_STOPPED"),
"Will also start stopped containers that were updated, if include-stopped is active") "Will also start stopped containers that were updated, if include-stopped is active")
flags.BoolP( flags.BoolP(
"enable-lifecycle-hooks", "enable-lifecycle-hooks",
"", "",
viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), envBool("WATCHTOWER_LIFECYCLE_HOOKS"),
"Enable the execution of commands triggered by pre- and post-update lifecycle hooks") "Enable the execution of commands triggered by pre- and post-update lifecycle hooks")
flags.BoolP( flags.BoolP(
"rolling-restart", "rolling-restart",
"", "",
viper.GetBool("WATCHTOWER_ROLLING_RESTART"), envBool("WATCHTOWER_ROLLING_RESTART"),
"Restart containers one at a time") "Restart containers one at a time")
flags.BoolP( flags.BoolP(
"http-api-update", "http-api-update",
"", "",
viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"), envBool("WATCHTOWER_HTTP_API_UPDATE"),
"Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request")
flags.BoolP( flags.BoolP(
"http-api-metrics", "http-api-metrics",
"", "",
viper.GetBool("WATCHTOWER_HTTP_API_METRICS"), envBool("WATCHTOWER_HTTP_API_METRICS"),
"Runs Watchtower with the Prometheus metrics API enabled") "Runs Watchtower with the Prometheus metrics API enabled")
flags.StringP( flags.StringP(
"http-api-token", "http-api-token",
"", "",
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), envString("WATCHTOWER_HTTP_API_TOKEN"),
"Sets an authentication token to HTTP API requests.") "Sets an authentication token to HTTP API requests.")
flags.BoolP( flags.BoolP(
"http-api-periodic-polls", "http-api-periodic-polls",
"", "",
viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"), envBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled") "Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
// https://no-color.org/ // https://no-color.org/
@ -173,18 +178,18 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
flags.StringP( flags.StringP(
"scope", "scope",
"", "",
viper.GetString("WATCHTOWER_SCOPE"), envString("WATCHTOWER_SCOPE"),
"Defines a monitoring scope for the Watchtower instance.") "Defines a monitoring scope for the Watchtower instance.")
flags.StringP( flags.StringP(
"porcelain", "porcelain",
"P", "P",
viper.GetString("WATCHTOWER_PORCELAIN"), envString("WATCHTOWER_PORCELAIN"),
`Write session results to stdout using a stable versioned format. Supported values: "v1"`) `Write session results to stdout using a stable versioned format. Supported values: "v1"`)
flags.String( flags.String(
"log-level", "log-level",
viper.GetString("WATCHTOWER_LOG_LEVEL"), envString("WATCHTOWER_LOG_LEVEL"),
"The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace") "The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace")
flags.BoolP( flags.BoolP(
@ -192,6 +197,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
"", "",
false, false,
"Do health check and exit") "Do health check and exit")
flags.BoolP(
"label-take-precedence",
"",
envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"),
"Label applied to containers take precedence over arguments")
} }
// RegisterNotificationFlags that are used by watchtower to send notifications // RegisterNotificationFlags that are used by watchtower to send notifications
@ -201,177 +212,202 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
flags.StringSliceP( flags.StringSliceP(
"notifications", "notifications",
"n", "n",
viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), envStringSlice("WATCHTOWER_NOTIFICATIONS"),
" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") " Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
flags.String( flags.String(
"notifications-level", "notifications-level",
viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), envString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
flags.IntP( flags.IntP(
"notifications-delay", "notifications-delay",
"", "",
viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"), envInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
"Delay before sending notifications, expressed in seconds") "Delay before sending notifications, expressed in seconds")
flags.StringP( flags.StringP(
"notifications-hostname", "notifications-hostname",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), envString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
"Custom hostname for notification titles") "Custom hostname for notification titles")
flags.StringP( flags.StringP(
"notification-email-from", "notification-email-from",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), envString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
"Address to send notification emails from") "Address to send notification emails from")
flags.StringP( flags.StringP(
"notification-email-to", "notification-email-to",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), envString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
"Address to send notification emails to") "Address to send notification emails to")
flags.IntP( flags.IntP(
"notification-email-delay", "notification-email-delay",
"", "",
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), envInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
"Delay before sending notifications, expressed in seconds") "Delay before sending notifications, expressed in seconds")
flags.StringP( flags.StringP(
"notification-email-server", "notification-email-server",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
"SMTP server to send notification emails through") "SMTP server to send notification emails through")
flags.IntP( flags.IntP(
"notification-email-server-port", "notification-email-server-port",
"", "",
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), envInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
"SMTP server port to send notification emails through") "SMTP server port to send notification emails through")
flags.BoolP( flags.BoolP(
"notification-email-server-tls-skip-verify", "notification-email-server-tls-skip-verify",
"", "",
viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"), envBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
`Controls whether watchtower verifies the SMTP server's certificate chain and host name. `Controls whether watchtower verifies the SMTP server's certificate chain and host name.
Should only be used for testing.`) Should only be used for testing.`)
flags.StringP( flags.StringP(
"notification-email-server-user", "notification-email-server-user",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
"SMTP server user for sending notifications") "SMTP server user for sending notifications")
flags.StringP( flags.StringP(
"notification-email-server-password", "notification-email-server-password",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
"SMTP server password for sending notifications") "SMTP server password for sending notifications")
flags.StringP( flags.StringP(
"notification-email-subjecttag", "notification-email-subjecttag",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), envString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
"Subject prefix tag for notifications via mail") "Subject prefix tag for notifications via mail")
flags.StringP( flags.StringP(
"notification-slack-hook-url", "notification-slack-hook-url",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), envString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
"The Slack Hook URL to send notifications to") "The Slack Hook URL to send notifications to")
flags.StringP( flags.StringP(
"notification-slack-identifier", "notification-slack-identifier",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), envString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
"A string which will be used to identify the messages coming from this watchtower instance") "A string which will be used to identify the messages coming from this watchtower instance")
flags.StringP( flags.StringP(
"notification-slack-channel", "notification-slack-channel",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"), envString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
"A string which overrides the webhook's default channel. Example: #my-custom-channel") "A string which overrides the webhook's default channel. Example: #my-custom-channel")
flags.StringP( flags.StringP(
"notification-slack-icon-emoji", "notification-slack-icon-emoji",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"), envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
"An emoji code string to use in place of the default icon") "An emoji code string to use in place of the default icon")
flags.StringP( flags.StringP(
"notification-slack-icon-url", "notification-slack-icon-url",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"), envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
"An icon image URL string to use in place of the default icon") "An icon image URL string to use in place of the default icon")
flags.StringP( flags.StringP(
"notification-msteams-hook", "notification-msteams-hook",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"), envString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
"The MSTeams WebHook URL to send notifications to") "The MSTeams WebHook URL to send notifications to")
flags.BoolP( flags.BoolP(
"notification-msteams-data", "notification-msteams-data",
"", "",
viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), envBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
"The MSTeams notifier will try to extract log entry fields as MSTeams message facts") "The MSTeams notifier will try to extract log entry fields as MSTeams message facts")
flags.StringP( flags.StringP(
"notification-gotify-url", "notification-gotify-url",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), envString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
"The Gotify URL to send notifications to") "The Gotify URL to send notifications to")
flags.StringP( flags.StringP(
"notification-gotify-token", "notification-gotify-token",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), envString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
"The Gotify Application required to query the Gotify API") "The Gotify Application required to query the Gotify API")
flags.BoolP( flags.BoolP(
"notification-gotify-tls-skip-verify", "notification-gotify-tls-skip-verify",
"", "",
viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), envBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"),
`Controls whether watchtower verifies the Gotify server's certificate chain and host name. `Controls whether watchtower verifies the Gotify server's certificate chain and host name.
Should only be used for testing.`) Should only be used for testing.`)
flags.String( flags.String(
"notification-template", "notification-template",
viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), envString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
"The shoutrrr text/template for the messages") "The shoutrrr text/template for the messages")
flags.StringArray( flags.StringArray(
"notification-url", "notification-url",
viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), envStringSlice("WATCHTOWER_NOTIFICATION_URL"),
"The shoutrrr URL to send notifications to") "The shoutrrr URL to send notifications to")
flags.Bool("notification-report", flags.Bool("notification-report",
viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"), envBool("WATCHTOWER_NOTIFICATION_REPORT"),
"Use the session report as the notification template data") "Use the session report as the notification template data")
flags.StringP( flags.StringP(
"notification-title-tag", "notification-title-tag",
"", "",
viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"), envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
"Title prefix tag for notifications") "Title prefix tag for notifications")
flags.Bool("notification-skip-title", flags.Bool("notification-skip-title",
viper.GetBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"), envBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
"Do not pass the title param to notifications") "Do not pass the title param to notifications")
flags.String( flags.String(
"warn-on-head-failure", "warn-on-head-failure",
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), envString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
"When to warn about HEAD pull requests failing. Possible values: always, auto or never") "When to warn about HEAD pull requests failing. Possible values: always, auto or never")
flags.Bool( flags.Bool(
"notification-log-stdout", "notification-log-stdout",
viper.GetBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"), envBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
"Write notification logs to stdout instead of logging (to stderr)") "Write notification logs to stdout instead of logging (to stderr)")
} }
func envString(key string) string {
viper.MustBindEnv(key)
return viper.GetString(key)
}
func envStringSlice(key string) []string {
viper.MustBindEnv(key)
return viper.GetStringSlice(key)
}
func envInt(key string) int {
viper.MustBindEnv(key)
return viper.GetInt(key)
}
func envBool(key string) bool {
viper.MustBindEnv(key)
return viper.GetBool(key)
}
func envDuration(key string) time.Duration {
viper.MustBindEnv(key)
return viper.GetDuration(key)
}
// SetDefaults provides default values for environment variables // SetDefaults provides default values for environment variables
func SetDefaults() { func SetDefaults() {
viper.AutomaticEnv() viper.AutomaticEnv()
@ -385,6 +421,7 @@ func SetDefaults() {
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "") viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower") viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info") viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto")
} }
// EnvConfig translates the command-line options into environment variables // EnvConfig translates the command-line options into environment variables
@ -473,14 +510,17 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) {
"notification-msteams-hook", "notification-msteams-hook",
"notification-gotify-token", "notification-gotify-token",
"notification-url", "notification-url",
"http-api-token",
} }
for _, secret := range secrets { for _, secret := range secrets {
getSecretFromFile(flags, secret) if err := getSecretFromFile(flags, secret); err != nil {
log.Fatalf("failed to get secret from flag %v: %s", secret, err)
}
} }
} }
// getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file. // getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file.
func getSecretFromFile(flags *pflag.FlagSet, secret string) { func getSecretFromFile(flags *pflag.FlagSet, secret string) error {
flag := flags.Lookup(secret) flag := flags.Lookup(secret)
if sliceValue, ok := flag.Value.(pflag.SliceValue); ok { if sliceValue, ok := flag.Value.(pflag.SliceValue); ok {
oldValues := sliceValue.GetSlice() oldValues := sliceValue.GetSlice()
@ -489,7 +529,7 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
if value != "" && isFile(value) { if value != "" && isFile(value) {
file, err := os.Open(value) file, err := os.Open(value)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
@ -499,25 +539,26 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
} }
values = append(values, line) values = append(values, line)
} }
if err := file.Close(); err != nil {
return err
}
} else { } else {
values = append(values, value) values = append(values, value)
} }
} }
sliceValue.Replace(values) return sliceValue.Replace(values)
return
} }
value := flag.Value.String() value := flag.Value.String()
if value != "" && isFile(value) { if value != "" && isFile(value) {
file, err := ioutil.ReadFile(value) content, err := os.ReadFile(value)
if err != nil { if err != nil {
log.Fatal(err) return err
}
err = flags.Set(secret, strings.TrimSpace(string(file)))
if err != nil {
log.Error(err)
} }
return flags.Set(secret, strings.TrimSpace(string(content)))
} }
return nil
} }
func isFile(s string) bool { func isFile(s string) bool {
@ -570,19 +611,59 @@ func ProcessFlagAliases(flags *pflag.FlagSet) {
// update schedule flag to match interval if it's set, or to the default if none of them are // update schedule flag to match interval if it's set, or to the default if none of them are
if intervalChanged || !scheduleChanged { if intervalChanged || !scheduleChanged {
interval, _ := flags.GetInt(`interval`) interval, _ := flags.GetInt(`interval`)
flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval)) _ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
} }
if flagIsEnabled(flags, `debug`) { if flagIsEnabled(flags, `debug`) {
flags.Set(`log-level`, `debug`) _ = flags.Set(`log-level`, `debug`)
} }
if flagIsEnabled(flags, `trace`) { if flagIsEnabled(flags, `trace`) {
flags.Set(`log-level`, `trace`) _ = flags.Set(`log-level`, `trace`)
} }
} }
// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger
func SetupLogging(f *pflag.FlagSet) error {
logFormat, _ := f.GetString(`log-format`)
noColor, _ := f.GetBool("no-color")
switch strings.ToLower(logFormat) {
case "auto":
// This will either use the "pretty" or "logfmt" format, based on whether the standard out is connected to a TTY
log.SetFormatter(&log.TextFormatter{
DisableColors: noColor,
// enable logrus built-in support for https://bixense.com/clicolors/
EnvironmentOverrideColors: true,
})
case "json":
log.SetFormatter(&log.JSONFormatter{})
case "logfmt":
log.SetFormatter(&log.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})
case "pretty":
log.SetFormatter(&log.TextFormatter{
// "Pretty" format combined with `--no-color` will only change the timestamp to the time since start
ForceColors: !noColor,
FullTimestamp: false,
})
default:
return fmt.Errorf("invalid log format: %s", logFormat)
}
rawLogLevel, _ := f.GetString(`log-level`)
if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
return fmt.Errorf("invalid log level: %e", err)
} else {
log.SetLevel(logLevel)
}
return nil
}
func flagIsEnabled(flags *pflag.FlagSet, name string) bool { func flagIsEnabled(flags *pflag.FlagSet, name string) bool {
value, err := flags.GetBool(name) value, err := flags.GetBool(name)
if err != nil { if err != nil {
@ -599,7 +680,7 @@ func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error
if flagValues, ok := flag.Value.(pflag.SliceValue); ok { if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
for _, value := range values { for _, value := range values {
flagValues.Append(value) _ = flagValues.Append(value)
} }
} else { } else {
return fmt.Errorf(`the value for flag %q is not a slice value`, name) return fmt.Errorf(`the value for flag %q is not a slice value`, name)

View file

@ -1,20 +1,22 @@
package flags package flags
import ( import (
"io/ioutil"
"os" "os"
"strings"
"testing" "testing"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestEnvConfig_Defaults(t *testing.T) { func TestEnvConfig_Defaults(t *testing.T) {
// Unset testing environments own variables, since those are not what is under test // Unset testing environments own variables, since those are not what is under test
os.Unsetenv("DOCKER_TLS_VERIFY") _ = os.Unsetenv("DOCKER_TLS_VERIFY")
os.Unsetenv("DOCKER_HOST") _ = os.Unsetenv("DOCKER_HOST")
cmd := new(cobra.Command) cmd := new(cobra.Command)
SetDefaults() SetDefaults()
@ -48,10 +50,7 @@ func TestEnvConfig_Custom(t *testing.T) {
func TestGetSecretsFromFilesWithString(t *testing.T) { func TestGetSecretsFromFilesWithString(t *testing.T) {
value := "supersecretstring" value := "supersecretstring"
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
require.NoError(t, err)
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
testGetSecretsFromFiles(t, "notification-email-server-password", value) testGetSecretsFromFiles(t, "notification-email-server-password", value)
} }
@ -60,18 +59,15 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) {
value := "megasecretstring" value := "megasecretstring"
// Create the temporary file which will contain a secret. // Create the temporary file which will contain a secret.
file, err := ioutil.TempFile(os.TempDir(), "watchtower-") file, err := os.CreateTemp(t.TempDir(), "watchtower-")
require.NoError(t, err) require.NoError(t, err)
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
// Write the secret to the temporary file. // Write the secret to the temporary file.
secret := []byte(value) _, err = file.Write([]byte(value))
_, err = file.Write(secret)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, file.Close())
err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name()) t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
require.NoError(t, err)
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
testGetSecretsFromFiles(t, "notification-email-server-password", value) testGetSecretsFromFiles(t, "notification-email-server-password", value)
} }
@ -80,16 +76,15 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
values := []string{"entry2", "", "entry3"} values := []string{"entry2", "", "entry3"}
// Create the temporary file which will contain a secret. // Create the temporary file which will contain a secret.
file, err := ioutil.TempFile(os.TempDir(), "watchtower-") file, err := os.CreateTemp(t.TempDir(), "watchtower-")
require.NoError(t, err) require.NoError(t, err)
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
// Write the secret to the temporary file. // Write the secret to the temporary file.
for _, value := range values { for _, value := range values {
_, err = file.WriteString("\n" + value) _, err = file.WriteString("\n" + value)
require.NoError(t, err) require.NoError(t, err)
} }
file.Close() require.NoError(t, file.Close())
testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`, testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`,
`--notification-url`, "entry1", `--notification-url`, "entry1",
@ -99,6 +94,7 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) { func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {
cmd := new(cobra.Command) cmd := new(cobra.Command)
SetDefaults() SetDefaults()
RegisterSystemFlags(cmd)
RegisterNotificationFlags(cmd) RegisterNotificationFlags(cmd)
require.NoError(t, cmd.ParseFlags(args)) require.NoError(t, cmd.ParseFlags(args))
GetSecretsFromFiles(cmd) GetSecretsFromFiles(cmd)
@ -166,9 +162,7 @@ func TestProcessFlagAliases(t *testing.T) {
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) { func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
cmd := new(cobra.Command) cmd := new(cobra.Command)
err := os.Setenv("WATCHTOWER_DEBUG", `true`) t.Setenv("WATCHTOWER_DEBUG", `true`)
require.NoError(t, err)
defer os.Unsetenv("WATCHTOWER_DEBUG")
SetDefaults() SetDefaults()
RegisterDockerFlags(cmd) RegisterDockerFlags(cmd)
@ -183,6 +177,57 @@ func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
assert.Equal(t, `debug`, logLevel) assert.Equal(t, `debug`, logLevel)
} }
func TestLogFormatFlag(t *testing.T) {
cmd := new(cobra.Command)
SetDefaults()
RegisterDockerFlags(cmd)
RegisterSystemFlags(cmd)
// Ensure the default value is Auto
require.NoError(t, cmd.ParseFlags([]string{}))
require.NoError(t, SetupLogging(cmd.Flags()))
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
// Test JSON format
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`}))
require.NoError(t, SetupLogging(cmd.Flags()))
assert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter)
// Test Pretty format
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`}))
require.NoError(t, SetupLogging(cmd.Flags()))
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
textFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
assert.True(t, ok)
assert.True(t, textFormatter.ForceColors)
assert.False(t, textFormatter.FullTimestamp)
// Test LogFmt format
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`}))
require.NoError(t, SetupLogging(cmd.Flags()))
textFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
assert.True(t, ok)
assert.True(t, textFormatter.DisableColors)
assert.True(t, textFormatter.FullTimestamp)
// Test invalid format
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`}))
require.Error(t, SetupLogging(cmd.Flags()))
}
func TestLogLevelFlag(t *testing.T) {
cmd := new(cobra.Command)
SetDefaults()
RegisterDockerFlags(cmd)
RegisterSystemFlags(cmd)
// Test invalid format
require.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`}))
require.Error(t, SetupLogging(cmd.Flags()))
}
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) { func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) } logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
cmd := new(cobra.Command) cmd := new(cobra.Command)
@ -202,9 +247,7 @@ func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) { func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {
cmd := new(cobra.Command) cmd := new(cobra.Command)
err := os.Setenv("WATCHTOWER_SCHEDULE", `@hourly`) t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
require.NoError(t, err)
defer os.Unsetenv("WATCHTOWER_SCHEDULE")
SetDefaults() SetDefaults()
RegisterDockerFlags(cmd) RegisterDockerFlags(cmd)
@ -234,3 +277,63 @@ func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
ProcessFlagAliases(flags) ProcessFlagAliases(flags)
}) })
} }
func TestFlagsArePrecentInDocumentation(t *testing.T) {
// Legacy notifcations are ignored, since they are (soft) deprecated
ignoredEnvs := map[string]string{
"WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI": "legacy",
"WATCHTOWER_NOTIFICATION_SLACK_ICON_URL": "legacy",
}
ignoredFlags := map[string]string{
"notification-gotify-url": "legacy",
"notification-slack-icon-emoji": "legacy",
"notification-slack-icon-url": "legacy",
}
cmd := new(cobra.Command)
SetDefaults()
RegisterDockerFlags(cmd)
RegisterSystemFlags(cmd)
RegisterNotificationFlags(cmd)
flags := cmd.PersistentFlags()
docFiles := []string{
"../../docs/arguments.md",
"../../docs/lifecycle-hooks.md",
"../../docs/notifications.md",
}
allDocs := ""
for _, f := range docFiles {
bytes, err := os.ReadFile(f)
if err != nil {
t.Fatalf("Could not load docs file %q: %v", f, err)
}
allDocs += string(bytes)
}
flags.VisitAll(func(f *pflag.Flag) {
if !strings.Contains(allDocs, "--"+f.Name) {
if _, found := ignoredFlags[f.Name]; !found {
t.Logf("Docs does not mention flag long name %q", f.Name)
t.Fail()
}
}
if !strings.Contains(allDocs, "-"+f.Shorthand) {
t.Logf("Docs does not mention flag shorthand %q (%q)", f.Shorthand, f.Name)
t.Fail()
}
})
for _, key := range viper.AllKeys() {
envKey := strings.ToUpper(key)
if !strings.Contains(allDocs, envKey) {
if _, found := ignoredEnvs[envKey]; !found {
t.Logf("Docs does not mention environment variable %q", envKey)
t.Fail()
}
}
}
}

BIN
oryxBuildBinary Executable file

Binary file not shown.

View file

@ -30,7 +30,7 @@ type Client interface {
StopContainer(t.Container, time.Duration) error StopContainer(t.Container, time.Duration) error
StartContainer(t.Container) (t.ContainerID, error) StartContainer(t.Container) (t.ContainerID, error)
RenameContainer(t.Container, string) error RenameContainer(t.Container, string) error
IsContainerStale(t.Container) (stale bool, latestImage t.ImageID, err error) IsContainerStale(t.Container, t.UpdateParams) (stale bool, latestImage t.ImageID, err error)
ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error) ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)
RemoveImageByID(t.ImageID) error RemoveImageByID(t.ImageID) error
WarnOnHeadPullFailed(container t.Container) bool WarnOnHeadPullFailed(container t.Container) bool
@ -230,9 +230,18 @@ func (client dockerClient) GetNetworkConfig(c t.Container) *network.NetworkingCo
} }
for _, ep := range config.EndpointsConfig { for _, ep := range config.EndpointsConfig {
// This keeps accumulating across upgrades with no apparent added value aliases := make([]string, 0, len(ep.Aliases))
// so throwing the information away to prevent overflows. cidAlias := c.ID().ShortID()
ep.Aliases = nil
// Remove the old container ID alias from the network aliases, as it would accumulate across updates otherwise
for _, alias := range ep.Aliases {
if alias == cidAlias {
continue
}
aliases = append(aliases, alias)
}
ep.Aliases = aliases
} }
return config return config
} }
@ -308,10 +317,10 @@ func (client dockerClient) RenameContainer(c t.Container, newName string) error
return client.api.ContainerRename(bg, string(c.ID()), newName) return client.api.ContainerRename(bg, string(c.ID()), newName)
} }
func (client dockerClient) IsContainerStale(container t.Container) (stale bool, latestImage t.ImageID, err error) { func (client dockerClient) IsContainerStale(container t.Container, params t.UpdateParams) (stale bool, latestImage t.ImageID, err error) {
ctx := context.Background() ctx := context.Background()
if !client.PullImages || container.IsNoPull() { if container.IsNoPull(params) {
log.Debugf("Skipping image pull.") log.Debugf("Skipping image pull.")
} else if err := client.PullImage(ctx, container); err != nil { } else if err := client.PullImage(ctx, container); err != nil {
return false, container.SafeImageID(), err return false, container.SafeImageID(), err

View file

@ -317,19 +317,20 @@ var _ = Describe("the client", func() {
}) })
Describe(`GetNetworkConfig`, func() { Describe(`GetNetworkConfig`, func() {
When(`providing a container with network aliases`, func() { When(`providing a container with network aliases`, func() {
It(`should purge the aliases`, func() { It(`should omit the container ID alias`, func() {
aliases := []string{"One", "Two"}
client := dockerClient{ client := dockerClient{
api: docker, api: docker,
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false}, ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false},
} }
container := MockContainer(WithImageName("docker.io/prefix/imagename:latest")) container := MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
aliases := []string{"One", "Two", container.ID().ShortID(), "Four"}
endpoints := map[string]*network.EndpointSettings{ endpoints := map[string]*network.EndpointSettings{
`test`: {Aliases: aliases}, `test`: {Aliases: aliases},
} }
container.containerInfo.NetworkSettings = &types.NetworkSettings{Networks: endpoints} container.containerInfo.NetworkSettings = &types.NetworkSettings{Networks: endpoints}
Expect(container.ContainerInfo().NetworkSettings.Networks[`test`].Aliases).To(Equal(aliases)) Expect(container.ContainerInfo().NetworkSettings.Networks[`test`].Aliases).To(Equal(aliases))
Expect(client.GetNetworkConfig(container).EndpointsConfig[`test`].Aliases).To(BeEmpty()) Expect(client.GetNetworkConfig(container).EndpointsConfig[`test`].Aliases).To(Equal([]string{"One", "Two", "Four"}))
}) })
}) })
}) })

View file

@ -2,12 +2,14 @@
package container package container
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/containrrr/watchtower/internal/util" "github.com/containrrr/watchtower/internal/util"
wt "github.com/containrrr/watchtower/pkg/types" wt "github.com/containrrr/watchtower/pkg/types"
"github.com/sirupsen/logrus"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container" dockercontainer "github.com/docker/docker/api/types/container"
@ -129,36 +131,31 @@ func (c Container) Enabled() (bool, bool) {
return parsedBool, true return parsedBool, true
} }
// IsMonitorOnly returns the value of the monitor-only label. If the label // IsMonitorOnly returns whether the container should only be monitored based on values of
// is not set then false is returned. // the monitor-only label, the monitor-only argument and the label-take-precedence argument.
func (c Container) IsMonitorOnly() bool { func (c Container) IsMonitorOnly(params wt.UpdateParams) bool {
rawBool, ok := c.getLabelValue(monitorOnlyLabel) return c.getContainerOrGlobalBool(params.MonitorOnly, monitorOnlyLabel, params.LabelPrecedence)
if !ok {
return false
} }
parsedBool, err := strconv.ParseBool(rawBool) // IsNoPull returns whether the image should be pulled based on values of
if err != nil { // the no-pull label, the no-pull argument and the label-take-precedence argument.
return false func (c Container) IsNoPull(params wt.UpdateParams) bool {
return c.getContainerOrGlobalBool(params.NoPull, noPullLabel, params.LabelPrecedence)
} }
return parsedBool func (c Container) getContainerOrGlobalBool(globalVal bool, label string, contPrecedence bool) bool {
if contVal, err := c.getBoolLabelValue(label); err != nil {
if !errors.Is(err, errorLabelNotFound) {
logrus.WithField("error", err).WithField("label", label).Warn("Failed to parse label value")
} }
return globalVal
// IsNoPull returns the value of the no-pull label. If the label is not set } else {
// then false is returned. if contPrecedence {
func (c Container) IsNoPull() bool { return contVal
rawBool, ok := c.getLabelValue(noPullLabel) } else {
if !ok { return contVal || globalVal
return false
} }
parsedBool, err := strconv.ParseBool(rawBool)
if err != nil {
return false
} }
return parsedBool
} }
// Scope returns the value of the scope UID label and if the label // Scope returns the value of the scope UID label and if the label

View file

@ -1,6 +1,7 @@
package container package container
import ( import (
"github.com/containrrr/watchtower/pkg/types"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@ -215,12 +216,13 @@ var _ = Describe("the container", func() {
}) })
When("checking no-pull label", func() { When("checking no-pull label", func() {
When("no-pull argument is not set", func() {
When("no-pull label is true", func() { When("no-pull label is true", func() {
c := MockContainer(WithLabels(map[string]string{ c := MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.no-pull": "true", "com.centurylinklabs.watchtower.no-pull": "true",
})) }))
It("should return true", func() { It("should return true", func() {
Expect(c.IsNoPull()).To(Equal(true)) Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(true))
}) })
}) })
When("no-pull label is false", func() { When("no-pull label is false", func() {
@ -228,7 +230,7 @@ var _ = Describe("the container", func() {
"com.centurylinklabs.watchtower.no-pull": "false", "com.centurylinklabs.watchtower.no-pull": "false",
})) }))
It("should return false", func() { It("should return false", func() {
Expect(c.IsNoPull()).To(Equal(false)) Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
}) })
}) })
When("no-pull label is set to an invalid value", func() { When("no-pull label is set to an invalid value", func() {
@ -236,13 +238,50 @@ var _ = Describe("the container", func() {
"com.centurylinklabs.watchtower.no-pull": "maybe", "com.centurylinklabs.watchtower.no-pull": "maybe",
})) }))
It("should return false", func() { It("should return false", func() {
Expect(c.IsNoPull()).To(Equal(false)) Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
}) })
}) })
When("no-pull label is unset", func() { When("no-pull label is unset", func() {
c = MockContainer(WithLabels(map[string]string{})) c = MockContainer(WithLabels(map[string]string{}))
It("should return false", func() { It("should return false", func() {
Expect(c.IsNoPull()).To(Equal(false)) Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
})
})
})
When("no-pull argument is set to true", func() {
When("no-pull label is true", func() {
c := MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.no-pull": "true",
}))
It("should return true", func() {
Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))
})
})
When("no-pull label is false", func() {
c := MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.no-pull": "false",
}))
It("should return true", func() {
Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))
})
})
When("label-take-precedence argument is set to true", func() {
When("no-pull label is true", func() {
c := MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.no-pull": "true",
}))
It("should return true", func() {
Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(true))
})
})
When("no-pull label is false", func() {
c := MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.no-pull": "false",
}))
It("should return false", func() {
Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(false))
})
})
}) })
}) })
}) })

View file

@ -5,3 +5,4 @@ import "errors"
var errorNoImageInfo = errors.New("no available image info") var errorNoImageInfo = errors.New("no available image info")
var errorNoContainerInfo = errors.New("no available container info") var errorNoContainerInfo = errors.New("no available container info")
var errorInvalidConfig = errors.New("container configuration missing or invalid") var errorInvalidConfig = errors.New("container configuration missing or invalid")
var errorLabelNotFound = errors.New("label was not found in container")

View file

@ -1,5 +1,7 @@
package container package container
import "strconv"
const ( const (
watchtowerLabel = "com.centurylinklabs.watchtower" watchtowerLabel = "com.centurylinklabs.watchtower"
signalLabel = "com.centurylinklabs.watchtower.stop-signal" signalLabel = "com.centurylinklabs.watchtower.stop-signal"
@ -55,3 +57,11 @@ func (c Container) getLabelValue(label string) (string, bool) {
val, ok := c.containerInfo.Config.Labels[label] val, ok := c.containerInfo.Config.Labels[label]
return val, ok return val, ok
} }
func (c Container) getBoolLabelValue(label string) (bool, error) {
if strVal, ok := c.containerInfo.Config.Labels[label]; ok {
value, err := strconv.ParseBool(strVal)
return value, err
}
return false, errorLabelNotFound
}

View file

@ -110,6 +110,7 @@ func createNotifier(urls []string, level log.Level, tplString string, legacy boo
legacyTemplate: legacy, legacyTemplate: legacy,
data: data, data: data,
params: params, params: params,
delay: delay,
} }
} }

View file

@ -112,7 +112,7 @@ var _ = Describe("the auth module", func() {
Expect(getScopeFromImageAuthURL("ghcr.io/containrrr/watchtower")).To(Equal("containrrr/watchtower")) Expect(getScopeFromImageAuthURL("ghcr.io/containrrr/watchtower")).To(Equal("containrrr/watchtower"))
}) })
}) })
It("should not crash when an empty field is recieved", func() { It("should not crash when an empty field is received", func() {
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",` input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",`
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower") imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
@ -120,7 +120,7 @@ var _ = Describe("the auth module", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(res).NotTo(BeNil()) Expect(res).NotTo(BeNil())
}) })
It("should not crash when a field without a value is recieved", func() { It("should not crash when a field without a value is received", func() {
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",valuelesskey` input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",valuelesskey`
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower") imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())

View file

@ -52,7 +52,7 @@ type Container interface {
SafeImageID() ImageID SafeImageID() ImageID
ImageName() string ImageName() string
Enabled() (bool, bool) Enabled() (bool, bool)
IsMonitorOnly() bool IsMonitorOnly(UpdateParams) bool
Scope() (string, bool) Scope() (string, bool)
Links() []string Links() []string
ToRestart() bool ToRestart() bool
@ -67,7 +67,7 @@ type Container interface {
VerifyConfiguration() error VerifyConfiguration() error
SetStale(bool) SetStale(bool)
IsStale() bool IsStale() bool
IsNoPull() bool IsNoPull(UpdateParams) bool
SetLinkedToRestarting(bool) SetLinkedToRestarting(bool)
IsLinkedToRestarting() bool IsLinkedToRestarting() bool
PreUpdateTimeout() int PreUpdateTimeout() int

View file

@ -11,6 +11,8 @@ type UpdateParams struct {
NoRestart bool NoRestart bool
Timeout time.Duration Timeout time.Duration
MonitorOnly bool MonitorOnly bool
NoPull bool
LifecycleHooks bool LifecycleHooks bool
RollingRestart bool RollingRestart bool
LabelPrecedence bool
} }